Breakwater logo: a wave breaking on a tetrapod

Breakwater

Foundry-style invariant fuzzing for Soroban

Demo video

The HackMeridian 2026 submission demo (3:43).

Loads a Vimeo player; Vimeo's privacy policy applies. Or watch it on vimeo.com.

The problem

Soroban has static analysis, AI review and formal verification, but no reusable stateful fuzzing. Breakwater declares actors (who can call), handlers (what they can call) and invariants (what must always hold), and ships two attack packs for risks that are specific to Stellar.

policy-bypass

Risk it hunts
Multi-step sequences that slip past OpenZeppelin smart-account policies (spending limits, context rules, signer scopes).
Why it matters
These policies are the safety layer for AI-agent wallets paying over x402 / MPP. A policy that holds for one transaction may fail for five.

ttl-archival

Risk it hunts
Contract state that expires under Soroban's TTL-based state archival while the business logic still depends on it.
Why it matters
A one-year payment stream whose storage entry is archived at month six is a correctness bug no EVM tool knows to look for.

How it works

A suite is an ordinary cargo test. Every run takes a seed, and a failure prints the seed to reproduce it with.

  1. DeclareA Handler for each contract call, an Actor for each caller, an Invariant for each property that must hold.
  2. GenerateSeeded multi-step sequences from proptest-state-machine against a local Soroban Env; ledger sequence and timestamp advance between steps.
  3. CheckEvery invariant after every step, with authorization through the real __check_auth path for the policy pack.
  4. ShrinkA failing sequence is shrunk to the shortest reproduction.
  5. Exportbreakwater export writes it as a self-contained #[test] you can drop into your crate.
  6. ReplayThe test replays exactly the recorded steps and asserts the same violation; the policy bypass was replayed on Testnet.

Findings

Every payment passes alone; the sequence does not. Each number comes from a command anyone can rerun.

49 → 49 → 49

policy-bypass · seed 7

Three payments of 49 USDC, the third exactly one 1 728-ledger window after the first: 147 USDC in a day against a cap meant as 100. The window was configured as 1 728 ledgers instead of 17 280; the policy code is correct, the configuration is not.

HTML report

275 days

ttl-archival · seed 11

A payment stream that never calls extend_ttl is archived with 275 days left to run, shrunk to two calls.

HTML report

1.3 days

ttl-archival · seed 5

An unauthenticated bump_ttl, callable by anyone, keeps a stream alive 1.3 days past its end.

5 vs 3

ttl-archival · seed 3

After a quiet quarter, one withdraw_many restores 5 archived entries against a budget of 3, a threshold the suite sets.

Testnet evidence

The shrunk bypass replayed on Stellar Testnet through the real OpenZeppelin smart account, signed by the agent's Ed25519 key: 147 USDC in 3 537 ledgers (about 4.9 hours), all accepted by the on-chain policies.

All seven contracts and five transactions are listed under Testnet evidence in the docs.

Quick start

Rust stable with the wasm32v1-none target and the Stellar CLI; see Getting started.

git clone https://github.com/Belvast-Innovation/Breakwater
cd Breakwater
cargo build --workspace

# Run the bundled demos (each drives `cargo test --test suite` in the example crate)
cargo run -p breakwater-cli -- run examples/policy-bypass --seed 7
cargo run -p breakwater-cli -- run examples/ttl-archival --seed 11

# Export the shrunk failure as a regression test and render a report
cargo run -p breakwater-cli -- export examples/policy-bypass --seed 7 --out examples/policy-bypass/tests/repro_policy_bypass_7.rs
cargo run -p breakwater-cli -- run examples/policy-bypass --seed 7 --json run.json
cargo run -p breakwater-cli -- report run.json --html report.html

Roadmap

Near term. The full list is in the docs.

  • Rule-rotation probe for policy-bypass: generate remove_context_rule + add_context_rule under real auth, since a re-created rule starts SpendingLimit with an empty history
  • Malicious keep-alive: bias generators toward unbounded extend_ttl calls on every public entry point, building on NoUnboundedTtlExtension
  • Restoration-cost budget as a CI gate: per-step resource ceilings from RestoreCostWithinBudget, compared run over run
  • Export of stellar-cli replay scripts and unsigned transaction XDR next to the Rust reproduction