Commit 01573cd
authored
feat(fuzz): add cargo-fuzz harnesses for contract entry-points (#351)
* feat(fuzz): add cargo-fuzz harnesses for all contract entry-points
Add cargo-fuzz fuzz/ directories for vault, looping, and rewards contracts:
vault/fuzz/fuzz_targets/:
- fuzz_vault_deposit.rs — arbitrary (amount) inputs; asserts positive
amounts succeed and balance is correct; non-positive may panic.
- fuzz_vault_withdraw.rs — arbitrary (initial_deposit, withdraw_amount);
asserts balance never goes negative.
looping/fuzz/fuzz_targets/:
- fuzz_looping_open_position.rs — arbitrary (collateral, leverage); asserts
valid inputs return monotonically-increasing position IDs.
rewards/fuzz/fuzz_targets/:
- fuzz_rewards_accrue_claim.rs — arbitrary (accrual_a, accrual_b); asserts
pending == sum of accruals, claim returns full pending, resets to 0.
Each fuzz/ directory includes:
- Cargo.toml with cargo-fuzz metadata and correct path dependencies
- fuzz_targets/*.rs harness files
- corpus/ seed inputs for fast initial coverage
Add .github/workflows/soroban_fuzz.yml:
- Smoke-fuzz loop: 60 s per entry-point on push/PR to main
- Nightly Rust toolchain (required by libfuzzer-sys)
- Crash corpus auto-uploaded as workflow artifact on failure
- workflow_dispatch with configurable fuzz_time for longer runs
Closes #249
* fix(fuzz): fix CI failures in fuzz workflow and harnesses
- Remove --locked from cargo-fuzz install to allow dependency resolution
with the runner's nightly toolchain (cargo-fuzz 0.13.2 with --locked
fails to compile on nightly-2025-06-01)
- Use dtolnay/rust-toolchain@nightly (latest) instead of pinned date
- Narrow soroban_fuzz.yml path trigger to fuzz/** only so it does not
fire on every contract change
- Rewrite all fuzz harnesses to use env.try_invoke_contract / invoke_contract
directly instead of generated client types (VaultContractClient etc.),
which are only available when soroban-sdk testutils compiles the parent
crate — avoids the soroban-env-host v22.1.3 testutils compile bug
- Add corpus directory path fix in upload-artifact steps
* fix(fuzz): declare fuzz targets, allow rlib for crates, fix symbol bindings
- Add [[bin]] entries to vault/looping/rewards fuzz Cargo.tomls so cargo-fuzz
can locate the targets (fixes the manifest-parse CI failure).
- Change contract crate-type from ["cdylib"] to ["rlib", "cdylib"] so the
fuzz crate can `use vault::VaultContract` (and looping/rewards) directly.
- Use Symbol::new(&env, "open_position") and Symbol::new(&env,
"pending_rewards") in the looping/rewards fuzz harnesses; the previous
symbol_short! bindings either pointed at a non-existent function
(open_pos vs open_position) or exceeded the 9-char limit
(pending_re -> compile-time panic from symbol_short!).
- Pin rand_core <= 0.6 in the fuzz crates, with a comment explaining the
testutils / rand_core 0.6 vs 0.10 mismatch in soroban-env-host 22.1.3.
* ci(fuzz): pin rand_core to 0.6.4 in cargo-fuzz jobs
soroban-env-host 22.1.3 testutils.rs calls
ed25519_dalek::SigningKey::generate(&mut ChaCha20Rng)
which transitively forces the crate graph to unify on the
rand_core 0.6 family. Without this pin, a transitive dep can
resolve to a newer rand_core major (0.7+) and the trait bound
ChaCha20Rng: ed25519_dalek::rand_core::CryptoRng stops being
satisfied, breaking every fuzz target.
Add a step between `Install cargo-fuzz` and `Run fuzz target`
that runs `cargo update -p rand_core --precise 0.6.4` so the
dep tree unifies on rand_core 0.6.x regardless of what other
transitive deps want.
Only affects the host-target fuzz builds; checked that adding
fuzz crates as workspace members would regress the wasm32
build (libfuzzer-sys is host-only).
* ci(fuzz): disambiguate pin; lock ed25519-dalek to 2.1.1
The previous `cargo update -p rand_core --precise 0.6.4` step failed
because three rand_core majors co-exist in the fuzz dep tree
(0.6.4, 0.9.5, 0.10.1) and cargo refuses an ambiguous spec.
Root cause: ed25519-dalek 3.0.0 is in the tree via soroban-env-host
22.1.3. Its dep chain
ed25519-dalek 3.0.0
-> curve25519-dalek 5.0.0
-> digest 0.11.3
-> crypto-common 0.2.2
-> rand_core 0.10.1
is what pulls rand_core 0.10.1, breaking
ChaCha20Rng: ed25519_dalek::rand_core::CryptoRng
in testutils.rs because ChaCha20Rng (from rand_chacha 0.3.1)
implements the rand_core 0.6 CryptoRng, not 0.10.
Fix: pin `ed25519-dalek@3.0.0` to `2.1.1` exactly, satisfying
soroban-env-host 22.1.3's >=2.0 constraint. The chain collapses
because ed25519-dalek 2.1.1 uses curve25519-dalek 4.x (rand_core 0.6).
Two further `cargo update` invocations are chained with `|| true`
as fallback safety nets for any residual rand_core majors that cargo
allows us to demote directly.
Cargo is invoked from the fuzz crate directory
(quantara/soroban/contracts/{vault,looping,rewards}/fuzz/) thanks
to the job-level `defaults.run.working-directory` already in place.
* ci(fuzz): cd fuzz before pin step so we mutate the right lockfile
Each fuzz crate (vault/fuzz, looping/fuzz, rewards/fuzz) declares its
own isolated workspace via `[workspace]` in its Cargo.toml, which means
it has its OWN `fuzz/Cargo.lock`. The job-level
`defaults.run.working-directory` puts the command at the parent
contract directory (`quantara/soroban/contracts/{vault,looping,rewards}/`),
one level above `fuzz/`. Without `cd fuzz`, `cargo update` was
mutating the parent contract workspace's `Cargo.lock` rather than
the fuzz crate's lockfile, so `cargo fuzz run` (one step later) saw
a fresh resolution and pulled `ed25519-dalek 3.0.0` again,
re-introducing the rand_core 0.6 / 0.10 trait-bound mismatch.
Adding `cd fuzz` before the four `cargo update --precise` calls makes
the lockfile mutation land in the right place. `cargo fuzz run` (which
cargo-fuzz derives from the `[package.metadata] cargo-fuzz = true`
marker) then reads the updated lockfile, so the pin takes effect.
* fix(fuzz): use soroban_sdk::IntoVal::into_val for numeric args
`soroban-sdk` 22.0.0 no longer auto-converts primitive numerics (i128,
u32) to `Val` via the `From`/`Into` trait family. The fuzz harnesses
were failing to compile with:
error[E0277]: the trait bound `soroban_sdk::Val: From<i128>` is not satisfied
--> fuzz_targets/fuzz_rewards_accrue_claim.rs:36:58
Fix: switch each numeric argument inside `soroban_sdk::vec![&env, ...]`
to `.into_val(&env)`, which goes through the SDK's `IntoVal` extension
trait (the canonical way to convert a host-side value into a contract
argument). Bring `IntoVal` into scope in each fuzz target's `use`
block. `user.to_val()` calls are already correct and were left alone.
Affected:
* vault/fuzz/fuzz_targets/fuzz_vault_deposit.rs (amount)
* vault/fuzz/fuzz_targets/fuzz_vault_withdraw.rs (initial_deposit, withdraw_amount)
* looping/fuzz/fuzz_targets/fuzz_looping_open_position.rs (collateral, leverage)
* rewards/fuzz/fuzz_targets/fuzz_rewards_accrue_claim.rs (accrual_a, accrual_b)
This is the surface-level error left after the cargo-fuzz dep-pin work
in the previous commits; once numeric args use IntoVal, the harnesses
should compile and run the 60-second smoke fuzz pass.
* fix(fuzz): explicit HostError generic on try_invoke_contract
`soroban-sdk` 22.x changed `Env::try_invoke_contract` to return
`Result<Result<T, ContractError>, HostError>`. The fuzz harnesses
were calling it as `env.try_invoke_contract::<T, _>(...)`, leaving
the host-error type uninferred; with multiple `impl TryFromVal<...>`
candidates for `Error`, rustc refused with:
error[E0283]: type annotations needed
--> fuzz_targets/fuzz_vault_withdraw.rs:42:13
|
42 | let _ = env.try_invoke_contract::<(), _>(
| ^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type of the error type `E`
In the looping harness, the same problem cascaded — the
single `.expect()` was returning `Result<u64, Error>` (the inner
layer) instead of `u64`, breaking the `position_id >= 1` and
`{position_id}` formatting assertions.
Fix: make the second generic argument explicit as
`soroban_sdk::Error` (which is the actual host-error type for the
outer Result), and where `.expect()` is also used, unwrap both layers:
// vault/deposit, vault/withdraw
- try_invoke_contract::<(), _>
+ try_invoke_contract::<(), soroban_sdk::Error>
// looping/open_position
- try_invoke_contract::<u64, _>
+ try_invoke_contract::<u64, soroban_sdk::Error>
- let position_id = result.expect("...");
+ let position_id = result
+ .expect("open_position returned a host error")
+ .expect("open_position with valid args returned a contract error");
The rewards harness only uses `env.invoke_contract` (infallible) and
needed no change.
---------
Co-authored-by: LaGodxy <LaGodxy@users.noreply.github.com>1 parent 2616d81 commit 01573cd
15 files changed
Lines changed: 561 additions & 3 deletions
File tree
- .github/workflows
- quantara/soroban/contracts
- looping
- fuzz
- corpus/fuzz_looping_open_position
- fuzz_targets
- rewards
- fuzz
- corpus/fuzz_rewards_accrue_claim
- fuzz_targets
- vault
- fuzz
- corpus
- fuzz_vault_deposit
- fuzz_vault_withdraw
- fuzz_targets
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
Binary file not shown.
Lines changed: 51 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
Binary file not shown.
0 commit comments