Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fuzz testing #1255

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
run: scarb fmt --check --workspace

- name: Run tests and generate coverage report
run: snforge test --workspace --coverage
run: snforge test --workspace --coverage --features fuzzing --fuzzer-runs 10000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the feeling that this will soon make tests run timeout in the action.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are not running on the PR btw, when that's fixed I'm curious to see how much time will this take with this few examples.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reduced the number of fuzzer runs to 500 which seems a reasonable value to me, at least for now

With 10k runs the tests took 1h 55m to complete which is way too much, here are the run results: https://github.com/OpenZeppelin/cairo-contracts/actions/runs/12374506824/job/34537193923

With 1k runs it took 17m 27s to run all the tests. 10 minutes more than our usual test runs.
Results: https://github.com/OpenZeppelin/cairo-contracts/actions/runs/12379685661/job/34554383936

Although it's an acceptable duration, it can increase much when we add more fuzz test cases. So now with 500 runs the tests job takes 12 minutes (5 minutes more than it was without fuzz tests)


- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
Expand Down
1 change: 1 addition & 0 deletions packages/testing/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod common;
pub mod constants;
pub mod deployment;
pub mod events;
pub mod math;
pub mod signing;

pub use common::{
Expand Down
16 changes: 16 additions & 0 deletions packages/testing/src/math.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use core::num::traits::ops::overflowing::{OverflowingAdd, OverflowingSub, OverflowingMul};
immrsd marked this conversation as resolved.
Show resolved Hide resolved

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
}
immrsd marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions packages/token/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ snforge_std.workspace = true
openzeppelin_testing = { path = "../testing" }
openzeppelin_test_common = { path = "../test_common" }

[features]
default = []
immrsd marked this conversation as resolved.
Show resolved Hide resolved
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;
145 changes: 145 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,145 @@
use ERC20Component::InternalTrait;
use crate::erc20::ERC20Component::{ERC20CamelOnlyImpl, ERC20Impl};
use crate::erc20::ERC20Component::{ERC20MetadataImpl, InternalImpl};
use openzeppelin_test_common::mocks::erc20::DualCaseERC20Mock;
use crate::erc20::ERC20Component;
use openzeppelin_testing::constants::{
OWNER, SPENDER, RECIPIENT, NAME, SYMBOL
};
use snforge_std::{test_address, start_cheat_caller_address};
use starknet::ContractAddress;
use openzeppelin_testing::math::{is_overflow_add, is_overflow_sub};

//
// 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);
assert_balance(owner, supply);
assert_balance(spender, 0);
assert_allowance(owner, spender, supply - spend_amount);
immrsd marked this conversation as resolved.
Show resolved Hide resolved
}

//
// 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