Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions .github/workflows/soroban_fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
name: Soroban Fuzz (Smoke — 60 s per target)

on:
push:
branches: [main]
paths:
- 'quantara/soroban/contracts/*/fuzz/**'
- '.github/workflows/soroban_fuzz.yml'
pull_request:
branches: [main]
paths:
- 'quantara/soroban/contracts/*/fuzz/**'
- '.github/workflows/soroban_fuzz.yml'
workflow_dispatch:
inputs:
fuzz_time:
description: 'Seconds to fuzz per target'
default: '60'
required: false

env:
CARGO_TERM_COLOR: always
FUZZ_TIME: ${{ github.event.inputs.fuzz_time || '60' }}

jobs:
# ── Vault fuzz targets ────────────────────────────────────────────────────
fuzz-vault:
name: Fuzz vault (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- fuzz_vault_deposit
- fuzz_vault_withdraw

defaults:
run:
working-directory: quantara/soroban/contracts/vault

steps:
- uses: actions/checkout@v4

- name: Install latest Rust nightly
uses: dtolnay/rust-toolchain@nightly

- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-fuzz-${{ github.sha }}
restore-keys: ${{ runner.os }}-cargo-fuzz-

- name: Install cargo-fuzz
run: cargo install cargo-fuzz

- name: Pin ed25519-dalek and rand_core to 0.6 family
# soroban-env-host 22.1.3's testutils.rs passes `ChaCha20Rng` into
# `ed25519_dalek::SigningKey::generate`, which requires CryptoRng
# from the rand_core 0.6 family. ed25519-dalek 3.x bumped to
# rand_core 0.10, breaking the trait bound. Pin ed25519-dalek to
# 2.1.1 (last 2.x release) and rand_core to 0.6.4; fall through
# with `|| true` for compatibility — cargo refuses to demote a
# rand_core 0.10 instance to 0.6.4 directly, but the ed25519-dalek
# pin upstream of it is the real lever.
# The fuzz crate is an isolated workspace ([workspace] in
# fuzz/Cargo.toml), so it has its OWN fuzz/Cargo.lock. Without
# `cd fuzz`, `cargo update` would mutate the parent contract
# workspace's lockfile, which `cargo fuzz run` ignores — hence
# the silent failure of the previous (no-cd) variant.
run: |
cd fuzz
cargo update -p ed25519-dalek@3.0.0 --precise 2.1.1 || true
cargo update -p ed25519-dalek --precise 2.1.1 || true
cargo update -p rand_core@0.10.1 --precise 0.6.4 || true
cargo update -p rand_core@0.9.5 --precise 0.6.4 || true

- name: Run fuzz target for ${{ env.FUZZ_TIME }}s
run: |
cargo fuzz run ${{ matrix.target }} \
-- -max_total_time=${{ env.FUZZ_TIME }} -jobs=1

- name: Upload crash corpus on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: vault-${{ matrix.target }}-crash-corpus
path: fuzz/corpus/${{ matrix.target }}
if-no-files-found: ignore

# ── Looping fuzz targets ───────────────────────────────────────────────────
fuzz-looping:
name: Fuzz looping (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- fuzz_looping_open_position

defaults:
run:
working-directory: quantara/soroban/contracts/looping

steps:
- uses: actions/checkout@v4

- name: Install latest Rust nightly
uses: dtolnay/rust-toolchain@nightly

- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-fuzz-${{ github.sha }}
restore-keys: ${{ runner.os }}-cargo-fuzz-

- name: Install cargo-fuzz
run: cargo install cargo-fuzz

- name: Pin ed25519-dalek and rand_core to 0.6 family
# soroban-env-host 22.1.3's testutils.rs passes `ChaCha20Rng` into
# `ed25519_dalek::SigningKey::generate`, which requires CryptoRng
# from the rand_core 0.6 family. ed25519-dalek 3.x bumped to
# rand_core 0.10, breaking the trait bound. Pin ed25519-dalek to
# 2.1.1 (last 2.x release) and rand_core to 0.6.4; fall through
# with `|| true` for compatibility — cargo refuses to demote a
# rand_core 0.10 instance to 0.6.4 directly, but the ed25519-dalek
# pin upstream of it is the real lever.
# The fuzz crate is an isolated workspace ([workspace] in
# fuzz/Cargo.toml), so it has its OWN fuzz/Cargo.lock. Without
# `cd fuzz`, `cargo update` would mutate the parent contract
# workspace's lockfile, which `cargo fuzz run` ignores — hence
# the silent failure of the previous (no-cd) variant.
run: |
cd fuzz
cargo update -p ed25519-dalek@3.0.0 --precise 2.1.1 || true
cargo update -p ed25519-dalek --precise 2.1.1 || true
cargo update -p rand_core@0.10.1 --precise 0.6.4 || true
cargo update -p rand_core@0.9.5 --precise 0.6.4 || true

- name: Run fuzz target for ${{ env.FUZZ_TIME }}s
run: |
cargo fuzz run ${{ matrix.target }} \
-- -max_total_time=${{ env.FUZZ_TIME }} -jobs=1

- name: Upload crash corpus on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: looping-${{ matrix.target }}-crash-corpus
path: fuzz/corpus/${{ matrix.target }}
if-no-files-found: ignore

# ── Rewards fuzz targets ───────────────────────────────────────────────────
fuzz-rewards:
name: Fuzz rewards (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- fuzz_rewards_accrue_claim

defaults:
run:
working-directory: quantara/soroban/contracts/rewards

steps:
- uses: actions/checkout@v4

- name: Install latest Rust nightly
uses: dtolnay/rust-toolchain@nightly

- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-fuzz-${{ github.sha }}
restore-keys: ${{ runner.os }}-cargo-fuzz-

- name: Install cargo-fuzz
run: cargo install cargo-fuzz

- name: Pin ed25519-dalek and rand_core to 0.6 family
# soroban-env-host 22.1.3's testutils.rs passes `ChaCha20Rng` into
# `ed25519_dalek::SigningKey::generate`, which requires CryptoRng
# from the rand_core 0.6 family. ed25519-dalek 3.x bumped to
# rand_core 0.10, breaking the trait bound. Pin ed25519-dalek to
# 2.1.1 (last 2.x release) and rand_core to 0.6.4; fall through
# with `|| true` for compatibility — cargo refuses to demote a
# rand_core 0.10 instance to 0.6.4 directly, but the ed25519-dalek
# pin upstream of it is the real lever.
# The fuzz crate is an isolated workspace ([workspace] in
# fuzz/Cargo.toml), so it has its OWN fuzz/Cargo.lock. Without
# `cd fuzz`, `cargo update` would mutate the parent contract
# workspace's lockfile, which `cargo fuzz run` ignores — hence
# the silent failure of the previous (no-cd) variant.
run: |
cd fuzz
cargo update -p ed25519-dalek@3.0.0 --precise 2.1.1 || true
cargo update -p ed25519-dalek --precise 2.1.1 || true
cargo update -p rand_core@0.10.1 --precise 0.6.4 || true
cargo update -p rand_core@0.9.5 --precise 0.6.4 || true

- name: Run fuzz target for ${{ env.FUZZ_TIME }}s
run: |
cargo fuzz run ${{ matrix.target }} \
-- -max_total_time=${{ env.FUZZ_TIME }} -jobs=1

- name: Upload crash corpus on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: rewards-${{ matrix.target }}-crash-corpus
path: fuzz/corpus/${{ matrix.target }}
if-no-files-found: ignore
2 changes: 1 addition & 1 deletion quantara/soroban/contracts/looping/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ license = "MIT"
publish = false

[lib]
crate-type = ["cdylib"]
crate-type = ["rlib", "cdylib"]
doctest = false

[dependencies]
Expand Down
30 changes: 30 additions & 0 deletions quantara/soroban/contracts/looping/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "looping-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
# Fuzz targets call contract functions via the Soroban test environment.
# testutils provides Env::default(), mock_all_auths(), and Address::generate().
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
looping = { path = ".." }
# Pin rand_core to <= 0.6 so the whole dep tree unifies on the rand_core
# 0.6 family. Without this, a transitive dep can resolve to rand_core 0.10,
# while soroban-env-host's `testutils` (which uses ChaCha20Rng through
# ed25519-dalek::rand_core) still expects the rand_core 0.6 `CryptoRng`
# trait, breaking compilation.
rand_core = "<0.7"

# Prevent this from interfering with the workspace.
[workspace]

[[bin]]
name = "fuzz_looping_open_position"
path = "fuzz_targets/fuzz_looping_open_position.rs"
test = false
doc = false
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! cargo-fuzz harness for LoopingContract::open_position entry-point.
//!
//! Invariants checked:
//! - Valid inputs (collateral > 0, leverage 100–500) must succeed.
//! - Returned position IDs are >= 1.
//!
//! Run:
//! ```bash
//! cargo +nightly fuzz run fuzz_looping_open_position -- -max_total_time=60
//! ```

#![no_main]

use libfuzzer_sys::fuzz_target;
use soroban_sdk::{testutils::Address as _, Address, Env, IntoVal, Symbol};
use looping::LoopingContract;

fuzz_target!(|data: &[u8]| {
if data.len() < 12 {
return;
}
let collateral = i64::from_le_bytes(data[..8].try_into().unwrap()) as i128;
let leverage = u32::from_le_bytes(data[8..12].try_into().unwrap());

let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(LoopingContract, ());
let user = Address::generate(&env);

// try_invoke_contract returns `Result<Result<u64, ContractError>, HostError>`;
// explicitly specify the host error type so type inference succeeds.
let result = env.try_invoke_contract::<u64, soroban_sdk::Error>(
&contract_id,
&Symbol::new(&env, "open_position"),
soroban_sdk::vec![
&env,
user.to_val(),
collateral.into_val(&env),
leverage.into_val(&env),
],
);

if collateral > 0 && (100..=500).contains(&leverage) {
// Unwrap the outer (host) Err first, then the inner (contract) Err.
let position_id = result
.expect("open_position returned a host error")
.expect("open_position with valid args returned a contract error");
assert!(position_id >= 1, "position_id must be >= 1, got {position_id}");
}
// Invalid inputs may error; no further assertion needed.
});
2 changes: 1 addition & 1 deletion quantara/soroban/contracts/rewards/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ license = "MIT"
publish = false

[lib]
crate-type = ["cdylib"]
crate-type = ["rlib", "cdylib"]
doctest = false

[dependencies]
Expand Down
30 changes: 30 additions & 0 deletions quantara/soroban/contracts/rewards/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "rewards-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
# Fuzz targets call contract functions via the Soroban test environment.
# testutils provides Env::default(), mock_all_auths(), and Address::generate().
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
rewards = { path = ".." }
# Pin rand_core to <= 0.6 so the whole dep tree unifies on the rand_core
# 0.6 family. Without this, a transitive dep can resolve to rand_core 0.10,
# while soroban-env-host's `testutils` (which uses ChaCha20Rng through
# ed25519-dalek::rand_core) still expects the rand_core 0.6 `CryptoRng`
# trait, breaking compilation.
rand_core = "<0.7"

# Prevent this from interfering with the workspace.
[workspace]

[[bin]]
name = "fuzz_rewards_accrue_claim"
path = "fuzz_targets/fuzz_rewards_accrue_claim.rs"
test = false
doc = false
Binary file not shown.
Loading
Loading