This repo uses coverage-guided fuzzing (libFuzzer via cargo-fuzz) to explore smart-contract edge cases that unit tests rarely hit (state-machine sequencing, time jumps, oracle manipulation, boundary math).
The fuzz targets live under stellar-lend/fuzz/.
Targets (one binary per contract area):
lending_actions— state-machine fuzzing forstellarlend-lendingamm_actions— action fuzzing forstellarlend-ammbridge_actions— action fuzzing forbridge
Each target interprets the input as a sequence of fixed-size 32-byte actions. This gives libFuzzer structure to mutate while still keeping the harness lightweight.
For performance and coverage, the fuzzer uses a compact action encoding:
- One input file =
Nactions - One action = 32 bytes (see
stellar-lend/fuzz/src/encoding.rs)
Each harness maps those bytes to protocol calls (deposit/borrow/repay/withdraw, swaps/liquidity, bridge operations) and validates basic invariants after the sequence.
The harnesses mutate ledger time via env.ledger().with_mut(|li| li.timestamp = ...) to exercise:
- interest accrual and timestamp math
- deadline / timeout style checks
The lending fuzzer registers a fuzz-only oracle contract (FuzzOracle) and can change per-asset prices on the fly.
This specifically targets view logic (collateral value, debt value, health factor) under adversarial price changes.
The *_actions targets are state-machine fuzzers. Inputs encode sequences, not single calls, so the fuzzer can reach deep interleavings:
- borrow → time jump → repay partial → withdraw → view reads
- pause toggles + retries
- repeated protocol config changes
To keep fuzzing fast and CI-friendly:
- actions per input are bounded
- time deltas are capped per step
- harnesses use
try_*contract calls where possible to avoid panics and keep exploration going
lending_actions implements a custom libFuzzer mutator in stellar-lend/fuzz/fuzz_targets/lending_actions.rs:
- keeps inputs aligned to 32-byte action boundaries
- performs small, field-aware mutations (kind/user/asset selectors, amount bytes, time bytes)
- occasionally grows/shrinks by one full action to explore different sequence lengths
This is intentionally protocol-aware: it helps libFuzzer spend more time exploring meaningful contract state transitions rather than breaking the input structure.
Seed corpora are checked into git:
stellar-lend/fuzz/corpus/lending_actions/stellar-lend/fuzz/corpus/amm_actions/stellar-lend/fuzz/corpus/bridge_actions/
A minimum corpus size is enforced by scripts/fuzz/check_corpus.sh (default: 10 files per target; configurable via MIN_CORPUS_FILES).
Prereqs:
- Rust nightly (
rustup toolchain install nightly) cargo-fuzz(cargo +nightly install cargo-fuzz)- LLVM/clang toolchain (required by libFuzzer)
Run:
cd stellar-lend
cargo +nightly fuzz run lending_actions -- -runs=50000 -timeout=5Other targets:
cd stellar-lend
cargo +nightly fuzz run amm_actions -- -runs=50000 -timeout=5
cargo +nightly fuzz run bridge_actions -- -runs=50000 -timeout=5When a crash is found, libFuzzer stores a reproducer in:
stellar-lend/fuzz/artifacts/<target>/
Use:
./scripts/fuzz/repro.sh lending_actions stellar-lend/fuzz/artifacts/lending_actions/crash-* -- -runs=1CI runs a smoke fuzz pass (bounded number of executions per target) via:
scripts/fuzz/check_corpus.shscripts/fuzz/run_ci_smoke.sh
This keeps the pipeline deterministic while still exercising the fuzz harnesses continuously.