Skip to content

Commit

Permalink
Fuzz testing (#1255)
Browse files Browse the repository at this point in the history
* Rename file with Math fuzz tests

* Turn on fuzz testing on CI

* Introduce fuzzing feature to token and utils packages

* Add overflow testing helpers

* Add fuzz tests for ERC20

* Add fuzz tests for ERC721Enumerable

* Move fuzz helpers to test_common

* Fix workflow file

* Remove specifying empty default features

* Lint files

* Update fuzz tests

* Remove coverage command from CI test run

* Improve ERC721Enum fuzz tests

* Reduce fuzzer runs from 10k to 1k

* Reduce fuzzer runs to 500

* Support unlimited allowance in test case
  • Loading branch information
immrsd authored Jan 9, 2025
1 parent 39950b7 commit ca212ce
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: scarb fmt --check --workspace

- name: Run tests
run: snforge test --workspace
run: snforge test --workspace --features fuzzing --fuzzer-runs 500

# Issue with cairo-coverage. Re-add to CI once issues are fixed.
#
Expand Down
1 change: 1 addition & 0 deletions packages/test_common/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod erc1155;
pub mod erc20;
pub mod erc721;
pub mod eth_account;
pub mod math;
pub mod mocks;
pub mod ownable;
pub mod upgrades;
Expand Down
16 changes: 16 additions & 0 deletions packages/test_common/src/math.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use core::num::traits::ops::overflowing::{OverflowingAdd, OverflowingMul, OverflowingSub};

pub fn is_overflow_add<T, +OverflowingAdd<T>, +Drop<T>>(x: T, y: T) -> bool {
let (_, does_overflow) = x.overflowing_add(y);
does_overflow
}

pub fn is_overflow_mul<T, +OverflowingMul<T>, +Drop<T>>(x: T, y: T) -> bool {
let (_, does_overflow) = x.overflowing_mul(y);
does_overflow
}

pub fn is_overflow_sub<T, +OverflowingSub<T>, +Drop<T>>(x: T, y: T) -> bool {
let (_, does_overflow) = x.overflowing_sub(y);
does_overflow
}
3 changes: 3 additions & 0 deletions packages/token/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ snforge_std.workspace = true
openzeppelin_testing = { path = "../testing" }
openzeppelin_test_common = { path = "../test_common" }

[features]
fuzzing = []

[lib]

[[target.starknet-contract]]
Expand Down
3 changes: 3 additions & 0 deletions packages/token/src/tests/erc20.cairo
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
mod test_erc20;
mod test_erc20_permit;

#[cfg(feature: 'fuzzing')]
mod test_fuzz_erc20;
165 changes: 165 additions & 0 deletions packages/token/src/tests/erc20/test_fuzz_erc20.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use ERC20Component::InternalTrait;
use core::num::traits::Bounded;
use crate::erc20::ERC20Component;
use crate::erc20::ERC20Component::{ERC20CamelOnlyImpl, ERC20Impl};
use crate::erc20::ERC20Component::{ERC20MetadataImpl, InternalImpl};
use openzeppelin_test_common::math::{is_overflow_add, is_overflow_sub};
use openzeppelin_test_common::mocks::erc20::DualCaseERC20Mock;
use openzeppelin_testing::constants::{NAME, OWNER, RECIPIENT, SPENDER, SYMBOL};
use snforge_std::{start_cheat_caller_address, test_address};
use starknet::ContractAddress;

//
// Setup
//

type ComponentState = ERC20Component::ComponentState<DualCaseERC20Mock::ContractState>;

fn COMPONENT_STATE() -> ComponentState {
ERC20Component::component_state_for_testing()
}

fn setup(supply: u256) -> ComponentState {
let mut state = COMPONENT_STATE();
state.initializer(NAME(), SYMBOL());
state.mint(OWNER(), supply);
state
}

//
// Tests
//

#[test]
fn test_mint(supply: u256, mint_amount: u256) {
if is_overflow_add(supply, mint_amount) {
return;
}
let mut state = setup(supply);

assert_total_supply(supply);
assert_balance(OWNER(), supply);

state.mint(RECIPIENT(), mint_amount);
assert_total_supply(supply + mint_amount);
assert_balance(RECIPIENT(), mint_amount);
}

#[test]
fn test_burn(supply: u256, burn_amount: u256) {
if is_overflow_sub(supply, burn_amount) {
return;
}
let mut state = setup(supply);

assert_total_supply(supply);
assert_balance(OWNER(), supply);

state.burn(OWNER(), burn_amount);
assert_total_supply(supply - burn_amount);
assert_balance(OWNER(), supply - burn_amount);
}

#[test]
fn test_mint_burn(initial_supply: u256, mint_amount: u256, burn_amount: u256) {
if is_overflow_add(initial_supply, mint_amount) {
return;
}
if is_overflow_sub(mint_amount, burn_amount) {
return;
}
let mut state = setup(initial_supply);
let (owner, recipient) = (OWNER(), RECIPIENT());

// Mint
state.mint(recipient, mint_amount);
assert_total_supply(initial_supply + mint_amount);
assert_balance(owner, initial_supply);
assert_balance(recipient, mint_amount);

// Burn
state.burn(recipient, burn_amount);
assert_total_supply(initial_supply + mint_amount - burn_amount);
assert_balance(owner, initial_supply);
assert_balance(recipient, mint_amount - burn_amount);
}

#[test]
fn test_transfer(supply: u256, transfer_amount: u256) {
if is_overflow_sub(supply, transfer_amount) {
return;
}
let mut state = setup(supply);
let (owner, recipient) = (OWNER(), RECIPIENT());

start_cheat_caller_address(test_address(), owner);
state.transfer(recipient, transfer_amount);

assert_balance(owner, supply - transfer_amount);
assert_balance(recipient, transfer_amount);
}

#[test]
fn test_transfer_from(supply: u256, transfer_amount: u256) {
if is_overflow_sub(supply, transfer_amount) {
return;
}
let mut state = setup(supply);
let (owner, spender, recipient) = (OWNER(), SPENDER(), RECIPIENT());
let contract_address = test_address();

// Approve
start_cheat_caller_address(contract_address, owner);
state.approve(spender, transfer_amount);
assert_balance(owner, supply);
assert_allowance(owner, spender, transfer_amount);

// Transfer from
start_cheat_caller_address(contract_address, spender);
state.transfer_from(owner, recipient, transfer_amount);
assert_allowance(owner, spender, 0);
assert_balance(owner, supply - transfer_amount);
assert_balance(recipient, transfer_amount);
assert_balance(spender, 0);
}

#[test]
fn test__spend_allowance(supply: u256, spend_amount: u256) {
if is_overflow_sub(supply, spend_amount) {
return;
}
let mut state = setup(supply);
let (owner, spender) = (OWNER(), SPENDER());
state._approve(owner, spender, supply);

state._spend_allowance(owner, spender, spend_amount);

// Allowance doesn't change if it's set to maximum
let expected_allowance = if supply == Bounded::MAX {
supply
} else {
supply - spend_amount
};
assert_balance(owner, supply);
assert_balance(spender, 0);
assert_allowance(owner, spender, expected_allowance);
}

//
// Helpers
//

fn assert_total_supply(expected: u256) {
let state = COMPONENT_STATE();
assert_eq!(state.total_supply(), expected);
}

fn assert_allowance(owner: ContractAddress, spender: ContractAddress, expected: u256) {
let state = COMPONENT_STATE();
assert_eq!(state.allowance(owner, spender), expected);
}

fn assert_balance(owner: ContractAddress, expected: u256) {
let state = COMPONENT_STATE();
assert_eq!(state.balance_of(owner), expected);
}
3 changes: 3 additions & 0 deletions packages/token/src/tests/erc721.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
mod test_erc721;
mod test_erc721_enumerable;
mod test_erc721_receiver;

#[cfg(feature: 'fuzzing')]
mod test_fuzz_erc721_enumerable;
Loading

0 comments on commit ca212ce

Please sign in to comment.