Skip to content
Open
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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ resolver = "2"
members = [
"contracts/*",
]
exclude = [
"contracts/sorosave/fuzz",
]

[workspace.dependencies]
soroban-sdk = "22.0.1"
Expand Down
2 changes: 1 addition & 1 deletion contracts/sorosave/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2021"
publish = false

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

[dependencies]
Expand Down
5 changes: 5 additions & 0 deletions contracts/sorosave/fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
target/
corpus/
artifacts/
coverage/
Cargo.lock
34 changes: 34 additions & 0 deletions contracts/sorosave/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "sorosave-fuzz"
version = "0.0.0"
edition = "2021"
publish = false

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
soroban-sdk = { version = "22.0.1", features = ["testutils"] }
sorosave = { path = ".." }

[[bin]]
name = "create_group"
path = "fuzz_targets/create_group.rs"
test = false
doc = false
bench = false

[[bin]]
name = "contribute"
path = "fuzz_targets/contribute.rs"
test = false
doc = false
bench = false

[[bin]]
name = "distribute_payout"
path = "fuzz_targets/distribute_payout.rs"
test = false
doc = false
bench = false
21 changes: 21 additions & 0 deletions contracts/sorosave/fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SoroSave Fuzz Targets

This directory configures `cargo-fuzz` targets for the core SoroSave contract flows:

- `create_group`: randomizes boundary values for group creation.
- `contribute`: randomizes member contribution order and repeated contribution attempts.
- `distribute_payout`: randomizes complete and incomplete payout attempts.

Run from `contracts/sorosave`:

```sh
cargo fuzz run create_group
cargo fuzz run contribute
cargo fuzz run distribute_payout
```

The fuzz package is excluded from the root workspace so normal `cargo test` and CI do not pull libFuzzer dependencies.

## Current Findings

No new contract issues are documented yet. These targets establish repeatable fuzz coverage for future edge-case discovery.
79 changes: 79 additions & 0 deletions contracts/sorosave/fuzz/fuzz_targets/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#![allow(dead_code)]

use soroban_sdk::{testutils::Address as _, token::StellarAssetClient, Address, Env};
use sorosave::{SoroSaveContract, SoroSaveContractClient};

pub struct Harness {
pub env: Env,
pub admin: Address,
pub client: SoroSaveContractClient<'static>,
pub token: Address,
}

pub fn setup() -> Harness {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let contract_id = env.register(SoroSaveContract, (&admin,));
let client = SoroSaveContractClient::new(&env, &contract_id);

let token_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(token_admin);
let token = token_id.address();
let token_client = StellarAssetClient::new(&env, &token);
token_client.mint(&admin, &1_000_000_000_000);

Harness {
env,
admin,
client,
token,
}
}

pub fn read_i64(data: &[u8], offset: usize) -> i64 {
let mut bytes = [0_u8; 8];
for (index, byte) in bytes.iter_mut().enumerate() {
*byte = data.get(offset + index).copied().unwrap_or_default();
}
i64::from_le_bytes(bytes)
}

pub fn read_u64(data: &[u8], offset: usize) -> u64 {
let mut bytes = [0_u8; 8];
for (index, byte) in bytes.iter_mut().enumerate() {
*byte = data.get(offset + index).copied().unwrap_or_default();
}
u64::from_le_bytes(bytes)
}

pub fn positive_amount(data: &[u8], offset: usize) -> i128 {
(read_u64(data, offset) % 5_000_000 + 1) as i128
}

pub fn cycle_length(data: &[u8], offset: usize) -> u64 {
read_u64(data, offset) % 604_800 + 1
}

pub fn member_count(data: &[u8], offset: usize) -> usize {
data.get(offset)
.map(|value| (value % 5 + 2) as usize)
.unwrap_or(2)
}

pub fn mint_members(
env: &Env,
token: &Address,
count: usize,
amount: i128,
) -> std::vec::Vec<Address> {
let token_client = StellarAssetClient::new(env, token);
let mut members = std::vec::Vec::with_capacity(count);
for _ in 0..count {
let member = Address::generate(env);
token_client.mint(&member, &amount);
members.push(member);
}
members
}
41 changes: 41 additions & 0 deletions contracts/sorosave/fuzz/fuzz_targets/contribute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#![no_main]

mod common;

use libfuzzer_sys::fuzz_target;
use soroban_sdk::{Address, String};

fuzz_target!(|data: &[u8]| {
let harness = common::setup();
let contribution_amount = common::positive_amount(data, 0);
let member_count = common::member_count(data, 8);
let members = common::mint_members(
&harness.env,
&harness.token,
member_count - 1,
contribution_amount * 4,
);

let group_id = harness.client.create_group(
&harness.admin,
&String::from_str(&harness.env, "Fuzz Contribute"),
&harness.token,
&contribution_amount,
&common::cycle_length(data, 9),
&(member_count as u32),
);

let mut actors = std::vec::Vec::<Address>::with_capacity(member_count);
actors.push(harness.admin.clone());
for member in members {
harness.client.join_group(&member, &group_id);
actors.push(member);
}

harness.client.start_group(&harness.admin, &group_id);

for byte in data.iter().skip(17).take(64) {
let actor = &actors[*byte as usize % actors.len()];
let _ = harness.client.try_contribute(actor, &group_id);
}
});
22 changes: 22 additions & 0 deletions contracts/sorosave/fuzz/fuzz_targets/create_group.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#![no_main]

mod common;

use libfuzzer_sys::fuzz_target;
use soroban_sdk::String;

fuzz_target!(|data: &[u8]| {
let harness = common::setup();
let contribution_amount = (common::read_i64(data, 0) % 5_000_000) as i128;
let cycle_length = common::read_u64(data, 8);
let max_members = data.get(16).map(|value| (value % 8) as u32).unwrap_or(0);

let _ = harness.client.try_create_group(
&harness.admin,
&String::from_str(&harness.env, "Fuzz Create"),
&harness.token,
&contribution_amount,
&cycle_length,
&max_members,
);
});
55 changes: 55 additions & 0 deletions contracts/sorosave/fuzz/fuzz_targets/distribute_payout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#![no_main]

mod common;

use libfuzzer_sys::fuzz_target;
use soroban_sdk::{Address, String};

fuzz_target!(|data: &[u8]| {
let harness = common::setup();
let contribution_amount = common::positive_amount(data, 0);
let member_count = common::member_count(data, 8);
let members = common::mint_members(
&harness.env,
&harness.token,
member_count - 1,
contribution_amount * 4,
);

let group_id = harness.client.create_group(
&harness.admin,
&String::from_str(&harness.env, "Fuzz Payout"),
&harness.token,
&contribution_amount,
&common::cycle_length(data, 9),
&(member_count as u32),
);

let mut actors = std::vec::Vec::<Address>::with_capacity(member_count);
actors.push(harness.admin.clone());
for member in members {
harness.client.join_group(&member, &group_id);
actors.push(member);
}

harness.client.start_group(&harness.admin, &group_id);

for (index, actor) in actors.iter().enumerate() {
let should_contribute = data
.get(17 + index)
.map(|byte| byte & 1 == 1)
.unwrap_or(true);
if should_contribute {
let _ = harness.client.try_contribute(actor, &group_id);
}
}

for byte in data.iter().skip(17 + actors.len()).take(16) {
if byte & 1 == 0 {
let _ = harness.client.try_distribute_payout(&group_id);
} else {
let actor = &actors[*byte as usize % actors.len()];
let _ = harness.client.try_contribute(actor, &group_id);
}
}
});