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
12 changes: 11 additions & 1 deletion .github/workflows/fuzz-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,17 @@ jobs:
working-directory: contracts/token-factory/fuzz
run: cargo +nightly run --release --bin fuzz_burn -- -max_len=1000 -timeout=60 -max_total_time=60
continue-on-error: true


- name: Run set_metadata fuzz target
working-directory: contracts/token-factory/fuzz
run: cargo +nightly run --release --bin fuzz_set_metadata -- -max_len=10000 -timeout=60 -max_total_time=60
continue-on-error: true

- name: Run mint_tokens fuzz target
working-directory: contracts/token-factory/fuzz
run: cargo +nightly run --release --bin fuzz_mint_tokens -- -max_len=1000 -timeout=60 -max_total_time=60
continue-on-error: true

- name: Upload fuzz artifacts on failure
if: failure()
uses: actions/upload-artifact@v7
Expand Down
12 changes: 12 additions & 0 deletions contracts/token-factory/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ path = "fuzz_targets/fuzz_burn.rs"
test = false
doc = false

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

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

[profile.release]
opt-level = 3

41 changes: 40 additions & 1 deletion contracts/token-factory/fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ This directory contains fuzz testing targets for the token factory contract usin

## Overview

Fuzz testing generates random inputs to discover edge cases and potential crashes in arithmetic and validation logic. The fuzz targets focus on three critical areas:
Fuzz testing generates random inputs to discover edge cases and potential crashes in arithmetic and validation logic. The fuzz targets focus on five critical areas:

1. **create_token**: UTF-8 string validation, name/symbol/decimals parsing, fee arithmetic with random inputs
2. **fee_arithmetic**: Integer overflow checking in fee calculations, saturation arithmetic, boundary conditions
3. **burn**: Burn amount arithmetic, balance invariants, total supply calculations with random amounts
4. **set_metadata**: Metadata URI string handling, fee comparison arithmetic, duplicate-set guard
5. **mint_tokens**: Amount validation, max-supply cap overflow (`checked_add`), fee distribution arithmetic

## Setup

Expand Down Expand Up @@ -102,6 +104,43 @@ cargo +nightly run --release --bin fuzz_create_token -- -max_len=10000 -timeout=

**File**: `fuzz_targets/fuzz_burn.rs`

### fuzz_set_metadata

**Focus**: Metadata URI string handling and fee arithmetic for `set_metadata`

**Tests**:
- Arbitrary byte sequences converted to UTF-8 metadata URIs (no length limit in contract)
- Fee sufficiency guard: `fee_payment >= metadata_fee`
- Fee remainder arithmetic after payment
- Duplicate-set guard simulation (idempotency path skips arithmetic)
- Saturating fee-accumulation operations matching `distribute_fee`

**Success Criteria**:
- No panics on any valid UTF-8 URI or fee combination
- Non-UTF-8 inputs handled without panic
- All arithmetic invariants maintained

**File**: `fuzz_targets/fuzz_set_metadata.rs`

### fuzz_mint_tokens

**Focus**: Amount validation, max-supply cap enforcement, and fee distribution for `mint_tokens`

**Tests**:
- Non-positive amount rejection (`amount <= 0`)
- Fee sufficiency guard: `fee_payment >= base_fee`
- `checked_add` overflow on `current_supply + amount` (maps to `ArithmeticOverflow` error)
- Max-supply cap enforcement: `new_total > cap` (maps to `MaxSupplyExceeded` error)
- Supply monotonicity: minting always increases total supply
- Fee split distribution arithmetic (two-recipient model)

**Success Criteria**:
- No panics on any `i128` amount or supply combination
- Overflow paths detected via `checked_add`, not via panic
- Supply invariants (`new_total <= cap`) maintained on success path

**File**: `fuzz_targets/fuzz_mint_tokens.rs`

## CI Integration

Fuzz tests are automatically run by GitHub Actions:
Expand Down
86 changes: 86 additions & 0 deletions contracts/token-factory/fuzz/fuzz_targets/fuzz_mint_tokens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#![no_main]

use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;

#[derive(Arbitrary, Debug, Clone)]
struct FuzzMintTokensInput {
amount: i128,
fee_payment: i128,
base_fee: i128,
// Simulates token_info.max_supply
max_supply: Option<i128>,
// Simulates already-minted supply tracked in storage
current_supply: i128,
}

fuzz_target!(|input: FuzzMintTokensInput| {
let base_fee = input.base_fee.saturating_abs();
let fee_payment = input.fee_payment;
let amount = input.amount;

// --- Guard: amount must be > 0 (mirrors `if amount <= 0` in mint_tokens) ---
if amount <= 0 {
// Contract returns InvalidParameters; no further arithmetic is performed.
return;
}

// --- Guard: fee must cover base_fee ---
if fee_payment < base_fee {
// Contract returns InsufficientFee.
return;
}

// After both guards pass, fee remainder must be non-negative.
let fee_remainder = fee_payment.saturating_sub(base_fee);
assert!(fee_remainder >= 0);

// --- Max-supply cap arithmetic (mirrors the checked_add in mint_tokens) ---
if let Some(cap) = input.max_supply {
// Only positive caps are meaningful; non-positive caps are invalid params
// in create_token/batch, so we can skip them here.
if cap <= 0 {
return;
}

let current = input.current_supply.max(0); // storage always holds >= 0

// This is the exact expression from lib.rs line 551:
// let new_total = current.checked_add(amount).ok_or(Error::ArithmeticOverflow)?;
match current.checked_add(amount) {
None => {
// Overflow path — contract returns ArithmeticOverflow, no panic.
return;
}
Some(new_total) => {
if new_total > cap {
// Contract returns MaxSupplyExceeded.
return;
}
// Successful path: new_total is within the cap.
assert!(new_total > current, "minting a positive amount must increase supply");
assert!(new_total <= cap, "supply must never exceed cap");
}
}
} else {
// No cap: unlimited mint — only the amount guard applies.
// Verify the mint amount itself doesn't silently wrap when accumulated.
let accumulated = amount.saturating_add(amount);
assert!(accumulated >= amount, "saturating_add must not decrease value for positive inputs");
}

// --- Fee distribution arithmetic (mirrors distribute_fee) ---
// Verify that splitting the fee across multiple recipients cannot panic.
// Model: two recipients each receiving 50 % (5_000 bps out of 10_000).
let share_a = fee_payment
.saturating_mul(5_000)
/ 10_000;
let share_b = fee_payment
.saturating_mul(5_000)
/ 10_000;
let distributed = share_a.saturating_add(share_b);
let _remainder = fee_payment.saturating_sub(distributed);

assert!(share_a >= 0 || fee_payment < 0);
assert!(share_b >= 0 || fee_payment < 0);
});
60 changes: 60 additions & 0 deletions contracts/token-factory/fuzz/fuzz_targets/fuzz_set_metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#![no_main]

use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;

#[derive(Arbitrary, Debug, Clone)]
struct FuzzSetMetadataInput {
// Random bytes for metadata URI - no length restriction in the contract
uri_bytes: Vec<u8>,
fee_payment: i128,
metadata_fee: i128,
// Whether a duplicate set attempt follows the first
attempt_duplicate: bool,
}

fuzz_target!(|input: FuzzSetMetadataInput| {
// Normalize the metadata_fee so it is non-negative (contract stores positive fees)
let metadata_fee = input.metadata_fee.saturating_abs();
let fee_payment = input.fee_payment;

// --- Fee comparison logic (mirrors set_metadata guard) ---
let fee_sufficient = fee_payment >= metadata_fee;

if fee_sufficient {
// Payment at or above the required fee must never underflow when subtracted
let remainder = fee_payment.saturating_sub(metadata_fee);
assert!(remainder >= 0);
}

// --- Metadata URI string validation ---
// The contract accepts any soroban_sdk::String; we model user-supplied bytes here.
let uri_str = match String::from_utf8(input.uri_bytes.clone()) {
Ok(s) => s,
// Non-UTF-8 bytes would be rejected at the SDK boundary — treat as invalid
Err(_) => return,
};

// Empty URIs are technically accepted by the contract (no length guard in set_metadata).
// Verify that working with the string does not panic regardless of content.
let _uri_len = uri_str.len();
let _is_empty = uri_str.is_empty();

// Simulate the "already set" idempotency guard: a second call for the same
// token must return MetadataAlreadySet without touching the fee.
if input.attempt_duplicate {
// After a successful first call the storage slot is occupied.
// The fee path is never reached, so no arithmetic is performed.
// Verify the fee values themselves are still well-formed.
assert!(metadata_fee >= 0);
}

// --- Overflow-safe fee accumulation (mirrors distribute_fee arithmetic) ---
// Ensure multiplying the fee by a small operation count cannot overflow.
let ops: i128 = 3; // set_metadata is a single operation, but guard the pattern
let _scaled = metadata_fee.saturating_mul(ops);
let _total = fee_payment.saturating_add(metadata_fee);

// Invariant: saturating operations always return a value
assert!(metadata_fee.saturating_add(i128::MAX) >= 0 || metadata_fee < 0);
});
Loading