From 2e1f82f4db4f1b2d88dfd4cafa06ff854626e20c Mon Sep 17 00:00:00 2001 From: Andrey Babushkin Date: Tue, 14 Jan 2025 17:32:33 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20Wake=20fuzzing=20cookbook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../context-based-balance-tracking-pattern.md | 30 ++++ .../differential-testing.md | 29 ++++ .../error-tolerance.md | 16 ++ .../advanced-testing-features/index.md | 8 + .../time-based-testing.md | 20 +++ ...token-allowances-with-multiple-branches.md | 31 ++++ .../account-balance-testing.md | 21 +++ .../cookbook/common-testing-patterns/index.md | 7 + .../multi-token-interaction.md | 21 +++ .../state-change-tracking.md | 28 ++++ .../test-flow-branching.md | 24 +++ .../basic-fuzz-test-structure.md | 36 ++++ .../essential-fundamentals/error-handling.md | 158 ++++++++++++++++++ docs/cookbook/essential-fundamentals/flows.md | 33 ++++ docs/cookbook/essential-fundamentals/index.md | 6 + docs/cookbook/index.md | 14 ++ ...dress-bytes-conversion-with-random-data.md | 29 ++++ .../cross-chain-message-passing.md | 35 ++++ .../deploy-with-proxy.md | 19 +++ docs/cookbook/specialized-use-cases/index.md | 9 + .../multi-chain-token-deployments.md | 33 ++++ .../multi-token-accounting.md | 22 +++ ...permit-functions-with-eip712-signatures.md | 45 +++++ docs/cookbook/testing-infrastructure/index.md | 7 + .../initialization-strategies.md | 93 +++++++++++ .../logging-with-formatting.md | 20 +++ .../post-sequence-cleanup.md | 20 +++ .../results-collection.md | 21 +++ mkdocs.yml | 34 ++++ 29 files changed, 869 insertions(+) create mode 100644 docs/cookbook/advanced-testing-features/context-based-balance-tracking-pattern.md create mode 100644 docs/cookbook/advanced-testing-features/differential-testing.md create mode 100644 docs/cookbook/advanced-testing-features/error-tolerance.md create mode 100644 docs/cookbook/advanced-testing-features/index.md create mode 100644 docs/cookbook/advanced-testing-features/time-based-testing.md create mode 100644 docs/cookbook/advanced-testing-features/token-allowances-with-multiple-branches.md create mode 100644 docs/cookbook/common-testing-patterns/account-balance-testing.md create mode 100644 docs/cookbook/common-testing-patterns/index.md create mode 100644 docs/cookbook/common-testing-patterns/multi-token-interaction.md create mode 100644 docs/cookbook/common-testing-patterns/state-change-tracking.md create mode 100644 docs/cookbook/common-testing-patterns/test-flow-branching.md create mode 100644 docs/cookbook/essential-fundamentals/basic-fuzz-test-structure.md create mode 100644 docs/cookbook/essential-fundamentals/error-handling.md create mode 100644 docs/cookbook/essential-fundamentals/flows.md create mode 100644 docs/cookbook/essential-fundamentals/index.md create mode 100644 docs/cookbook/index.md create mode 100644 docs/cookbook/specialized-use-cases/address-bytes-conversion-with-random-data.md create mode 100644 docs/cookbook/specialized-use-cases/cross-chain-message-passing.md create mode 100644 docs/cookbook/specialized-use-cases/deploy-with-proxy.md create mode 100644 docs/cookbook/specialized-use-cases/index.md create mode 100644 docs/cookbook/specialized-use-cases/multi-chain-token-deployments.md create mode 100644 docs/cookbook/specialized-use-cases/multi-token-accounting.md create mode 100644 docs/cookbook/specialized-use-cases/permit-functions-with-eip712-signatures.md create mode 100644 docs/cookbook/testing-infrastructure/index.md create mode 100644 docs/cookbook/testing-infrastructure/initialization-strategies.md create mode 100644 docs/cookbook/testing-infrastructure/logging-with-formatting.md create mode 100644 docs/cookbook/testing-infrastructure/post-sequence-cleanup.md create mode 100644 docs/cookbook/testing-infrastructure/results-collection.md diff --git a/docs/cookbook/advanced-testing-features/context-based-balance-tracking-pattern.md b/docs/cookbook/advanced-testing-features/context-based-balance-tracking-pattern.md new file mode 100644 index 000000000..eb27ef15e --- /dev/null +++ b/docs/cookbook/advanced-testing-features/context-based-balance-tracking-pattern.md @@ -0,0 +1,30 @@ +# Context-Based Balance Tracking Pattern + +Example of testing a token contract with a context-based balance tracking pattern. + +```python +class BalanceTracker: + def track_transfer(self, token, from_: Address, to: Address, + amount: int) -> tuple[int, int]: + before_from = token.balanceOf(from_) + before_to = token.balanceOf(to) + + yield + + after_from = token.balanceOf(from_) + after_to = token.balanceOf(to) + return (before_from - after_from, after_to - before_to) + +class BalanceTest(FuzzTest): + tracker: BalanceTracker + + @flow() + def flow_transfer(self): + with self.tracker.track_transfer(self.token, sender, receiver, + amount) as changes: + self.token.transfer(receiver, amount, from_=sender) + + delta_from, delta_to = changes + assert delta_from == amount + assert delta_to == amount +``` \ No newline at end of file diff --git a/docs/cookbook/advanced-testing-features/differential-testing.md b/docs/cookbook/advanced-testing-features/differential-testing.md new file mode 100644 index 000000000..82a8a6c03 --- /dev/null +++ b/docs/cookbook/advanced-testing-features/differential-testing.md @@ -0,0 +1,29 @@ +# Differential Testing + +Example of testing a token contract with a differential testing approach. + +```python +# Model class mirrors contract state +class TokenModel: + balances: dict[Address, int] + total_supply: int + + def transfer(self, from_: Address, to_: Address, amount: int): + self.balances[from_] -= amount + self.balances[to_] += amount + +class ModelBasedTest(FuzzTest): + token: Token + model: TokenModel + + @flow() + def flow_action(self): + # Perform action on both contract and model + self.token.transfer(to, amount) + self.model.transfer(to, amount) + + @invariant() + def invariant_state(self): + # Compare contract state with model + assert self.token.balanceOf(user) == self.model.balances[user] +``` \ No newline at end of file diff --git a/docs/cookbook/advanced-testing-features/error-tolerance.md b/docs/cookbook/advanced-testing-features/error-tolerance.md new file mode 100644 index 000000000..d423628cb --- /dev/null +++ b/docs/cookbook/advanced-testing-features/error-tolerance.md @@ -0,0 +1,16 @@ +# Error Tolerance + +Example of testing a contract with a defined error tolerance for invariants. + +```python +class PrecisionTest(FuzzTest): + ERROR_TOLERANCE = 10**10 # Define acceptable rounding error + + @invariant() + def invariant_with_tolerance(self): + contract_value = self.contract.getValue() + model_value = self.model.getValue() + + # Assert with tolerance + assert abs(contract_value - model_value) < self.ERROR_TOLERANCE +``` \ No newline at end of file diff --git a/docs/cookbook/advanced-testing-features/index.md b/docs/cookbook/advanced-testing-features/index.md new file mode 100644 index 000000000..df67e611d --- /dev/null +++ b/docs/cookbook/advanced-testing-features/index.md @@ -0,0 +1,8 @@ +# Advanced Testing Features +Sophisticated testing patterns for complex scenarios, including differential testing, precision handling, and time-based operations. These patterns help when basic testing approaches aren't sufficient. + +- [Context Based Balance Tracking Pattern](context-based-balance-tracking-pattern.md) +- [Differential Testing](differential-testing.md) +- [Error Tolerance](error-tolerance.md) +- [Time Based Testing](time-based-testing.md) +- [Token Allowances with Multiple Branches](token-allowances-with-multiple-branches.md) diff --git a/docs/cookbook/advanced-testing-features/time-based-testing.md b/docs/cookbook/advanced-testing-features/time-based-testing.md new file mode 100644 index 000000000..8b3bfac9b --- /dev/null +++ b/docs/cookbook/advanced-testing-features/time-based-testing.md @@ -0,0 +1,20 @@ +# Time-Based Testing + +Example of testing a contract with time-based operations. + +```python +class TimeBasedTest(FuzzTest): + day: int = 0 + + @flow(weight=lambda self: min(self.day * 0.1, 0.5)) + def flow_time_sensitive(self): + days_advance = random_int(1, 7) + + if self.day > 0: + chain.mine(lambda x: x + days_advance * 86400) + + self.day += days_advance + + # Perform time-sensitive operations + self.contract.update() +``` \ No newline at end of file diff --git a/docs/cookbook/advanced-testing-features/token-allowances-with-multiple-branches.md b/docs/cookbook/advanced-testing-features/token-allowances-with-multiple-branches.md new file mode 100644 index 000000000..d8d91cf7b --- /dev/null +++ b/docs/cookbook/advanced-testing-features/token-allowances-with-multiple-branches.md @@ -0,0 +1,31 @@ +# Token Allowances with Multiple Branches + +Example of testing a token contract with multiple branches in the transferFrom function. + +```python +@flow() +def flow_transfer_from(self) -> None: + sender = random_account() + recipient = random_account() + executor = random_account() + insufficient_allowance = random_bool(true_prob=0.15) + + if insufficient_allowance: + amount = random_int(self._allowances[sender][executor] + 1, 2**256 - 1) + insufficient_balance = False + else: + amount = random_int(0, min(self._allowances[sender][executor], self._balances[sender])) + insufficient_balance = random_bool(true_prob=0.15) + if insufficient_balance: + amount = random_int(self._balances[sender] + 1, 2**256 - 1) + + with may_revert() as e: + self.token.transferFrom(sender, recipient, amount, from_=executor) + + if insufficient_allowance or insufficient_balance: + assert e.value == Panic(PanicCodeEnum.UNDERFLOW_OVERFLOW) + else: + self._balances[sender] -= amount + self._balances[recipient] += amount + self._allowances[sender][executor] -= amount +``` diff --git a/docs/cookbook/common-testing-patterns/account-balance-testing.md b/docs/cookbook/common-testing-patterns/account-balance-testing.md new file mode 100644 index 000000000..a344c578a --- /dev/null +++ b/docs/cookbook/common-testing-patterns/account-balance-testing.md @@ -0,0 +1,21 @@ +# Account Balance Testing + +Testing account balances and registration in a contract that tracks user accounts and their balances. + +```python +class AccountManagementTest(FuzzTest): + accounts: dict[Address, bool] # Track active accounts + + def pre_sequence(self): + self.accounts = {} + + @flow() + def flow_register_account(self): + user = random_account(lower_bound=1) # Skip account 0 + self.accounts[user.address] = True + + @invariant() + def invariant_accounts(self): + for account in self.accounts: + assert self.contract.balanceOf(account) >= 0 +``` diff --git a/docs/cookbook/common-testing-patterns/index.md b/docs/cookbook/common-testing-patterns/index.md new file mode 100644 index 000000000..fcc8f2509 --- /dev/null +++ b/docs/cookbook/common-testing-patterns/index.md @@ -0,0 +1,7 @@ +# Common Testing Patterns +Frequently used testing approaches that apply to most smart contract testing scenarios, focusing on balance tracking, state changes, and common interactions. + +- [Account Balance Testing](account-balance-testing.md) +- [Multi Token Interaction](multi-token-interaction.md) +- [State Change Tracking](state-change-tracking.md) +- [Test Flow Branching](test-flow-branching.md) diff --git a/docs/cookbook/common-testing-patterns/multi-token-interaction.md b/docs/cookbook/common-testing-patterns/multi-token-interaction.md new file mode 100644 index 000000000..ecdba9ef4 --- /dev/null +++ b/docs/cookbook/common-testing-patterns/multi-token-interaction.md @@ -0,0 +1,21 @@ +# Multi-Token Interaction + +Example of testing a contract that interacts with multiple tokens. + +```python +class MultiTokenTest(FuzzTest): + token_a: Token + token_b: Token + + def random_amount(self) -> int: + return random_int(1, 10) * 10**18 # Handle decimals + + @flow() + def flow_swap(self): + amount = self.random_amount() + user = random_account() + + # Approve and swap + self.token_a.approve(self.pool, amount, from_=user) + self.pool.swap(self.token_a, self.token_b, amount, from_=user) +``` \ No newline at end of file diff --git a/docs/cookbook/common-testing-patterns/state-change-tracking.md b/docs/cookbook/common-testing-patterns/state-change-tracking.md new file mode 100644 index 000000000..4c85db250 --- /dev/null +++ b/docs/cookbook/common-testing-patterns/state-change-tracking.md @@ -0,0 +1,28 @@ +# State Change Tracking + +Example of tracking complex state changes in a contract. + +```python +class StateTracker: + claimed: int + total_reward: int + balances: dict[Address, int] + + def track_claim(self, user: Address, amount: int): + self.claimed += amount + self.balances[user] += amount + + +class StateTrackingTest(FuzzTest): + tracker: StateTracker + + @flow() + def flow_claim(self): + user = random_account() + before = self.token.balanceOf(user) + self.contract.claim(from_=user) + after = self.token.balanceOf(user) + + claimed = after - before + self.tracker.track_claim(user, claimed) +``` \ No newline at end of file diff --git a/docs/cookbook/common-testing-patterns/test-flow-branching.md b/docs/cookbook/common-testing-patterns/test-flow-branching.md new file mode 100644 index 000000000..a2e18fa1a --- /dev/null +++ b/docs/cookbook/common-testing-patterns/test-flow-branching.md @@ -0,0 +1,24 @@ +# Test Flow Branching + +Example of testing a contract with branching logic in the test flow. + +```python +class CrossAccountTest(FuzzTest): + @flow() + def flow_multi_account(self): + user_count = random_int(1, 5) + + for _ in range(user_count): + user = random_account(lower_bound=1).address + + if self.pool.balanceOf(user) == 0: + self.deposit(user, random_amount()) + else: + self.claim(user) + + if random_bool(): + self.deposit(user, random_amount()) + else: + balance = self.pool.balanceOf(user) + self.withdraw(user, min(random_amount(), balance)) +``` \ No newline at end of file diff --git a/docs/cookbook/essential-fundamentals/basic-fuzz-test-structure.md b/docs/cookbook/essential-fundamentals/basic-fuzz-test-structure.md new file mode 100644 index 000000000..b5459d75b --- /dev/null +++ b/docs/cookbook/essential-fundamentals/basic-fuzz-test-structure.md @@ -0,0 +1,36 @@ +# Basic Fuzz Test Structure + +The basic structure of a fuzz test. + +```python +from wake.testing import * +from wake.testing.fuzzing import * + +# Import the contract using pytypes path. +# The path is the same as the one used in the Solidity codebase. +from pytypes.contracts.Token import Token + + +class BasicFuzzTest(FuzzTest): + token: Token # Contract instance + owner: Account + + def pre_sequence(self): + self.owner = chain.accounts[0] + self.token = Token.deploy("Name", "SYM", from_=self.owner) + + @flow() + def flow_transfer(self): + amount = random_int(1, 1000) + user = random_account() + self.token.transfer(user, amount, from_=self.owner) + + @invariant() + def invariant_supply(self): + assert self.token.totalSupply() == self.initial_supply + + +@chain.connect() +def test_basic(): + BasicFuzzTest().run(sequences_count=1, flows_count=100) +``` \ No newline at end of file diff --git a/docs/cookbook/essential-fundamentals/error-handling.md b/docs/cookbook/essential-fundamentals/error-handling.md new file mode 100644 index 000000000..fedbfbed2 --- /dev/null +++ b/docs/cookbook/essential-fundamentals/error-handling.md @@ -0,0 +1,158 @@ +# Error Handling + +Example of testing a complex function with multiple branches, including handling errors and ensuring all branches are covered. + +```solidity +contract ComplexFunction { + error InsufficientBalance(); + error InvalidAmount(); + error Unauthorized(); + + function complexTransfer( + address from, + address to, + uint256 amount, + bytes calldata data + ) external { + if (amount == 0) revert InvalidAmount(); + if (balances[from] < amount) revert InsufficientBalance(); + if (!isAuthorized(msg.sender)) revert Unauthorized(); + + if (data.length > 0) { + // Complex path 1 + _handleData(data); + balances[from] -= amount; + balances[to] += amount; + } else { + // Complex path 2 + balances[from] -= amount; + balances[to] += amount; + } + } +} +``` + +```python +@flow() +def flow_complex_transfer(self) -> None: + # Test invalid amount branch + with must_revert(ComplexFunction.InvalidAmount) as e: + self.complex.complexTransfer( + sender, + recipient, + 0, + b"" + ) + + # Test insufficient balance branch + amount = random_int(self._balances[sender] + 1, 2**256 - 1) + with may_revert(ComplexFunction.InsufficientBalance) as e: + self.complex.complexTransfer( + sender, + recipient, + amount, + b"" + ) + self._balances[sender] -= amount + self._balances[recipient] += amount + + # Test unauthorized branch + unauthorized = random_account() + with must_revert(ComplexFunction.Unauthorized) as e: + self.complex.complexTransfer( + sender, + recipient, + 100, + b"", + from_=unauthorized + ) + + # Test successful data path + amount = random_int(0, self._balances[sender]) + data = random_bytes(1, 100) + + self.complex.complexTransfer( + sender, + recipient, + amount, + data, + from_=authorized + ) + + self._balances[sender] -= amount + self._balances[recipient] += amount + self._validate_data_processed(data) + + # Test successful no-data path + amount = random_int(0, self._balances[sender]) + self.complex.complexTransfer( + sender, + recipient, + amount, + b"", + from_=authorized + ) + + self._balances[sender] -= amount + self._balances[recipient] += amount +``` + +Example of testing complex inputs validation with different error handling scenarios. + +```solidity +// Example of complex inputs validation +contract ComplexInputs { + error InvalidSignature(); + error ExpiredDeadline(); + error InvalidNonce(); + + struct ComplexParams { + address owner; + uint256 value; + uint256 nonce; + uint256 deadline; + bytes signature; + } + + function validateAndExecute(ComplexParams calldata params) external { + if (params.deadline < block.timestamp) revert ExpiredDeadline(); + if (params.nonce != nonces[params.owner]) revert InvalidNonce(); + if (!_verify(params)) revert InvalidSignature(); + + // Execute logic + _execute(params); + } +} +``` + +```python +@flow() +def flow_validate_complex_inputs(self) -> None: + # Test expired deadline + params = self._generate_valid_params() + params.deadline = random_int(0, block.timestamp - 1) + + with must_revert(ComplexInputs.ExpiredDeadline): + self.complex.validateAndExecute(params) + + # Test invalid nonce + params = self._generate_valid_params() + params.nonce = self._nonces[params.owner] + 1 + + with must_revert(ComplexInputs.InvalidNonce): + self.complex.validateAndExecute(params) + + # Test invalid signature + params = self._generate_valid_params() + params.signature = random_bytes(65) + + with must_revert(ComplexInputs.InvalidSignature): + self.complex.validateAndExecute(params) + + # Test successful path + params = self._generate_valid_params() + self.complex.validateAndExecute(params) + + self._nonces[params.owner] += 1 + self._validate_execution(params) +``` diff --git a/docs/cookbook/essential-fundamentals/flows.md b/docs/cookbook/essential-fundamentals/flows.md new file mode 100644 index 000000000..ce75aa84d --- /dev/null +++ b/docs/cookbook/essential-fundamentals/flows.md @@ -0,0 +1,33 @@ +# Flows + +Generation of flows for external functions and view functions. + +```solidity +contract A { + uint256 public a; + function foo() external returns (uint256) { a = 1; } +} + +contract B is A { + function bar() external returns (uint256) { a = 2; } + function view_baz() external view returns (uint256) { return 3; } +} +``` + +```python +class BTest(FuzzTest): + b: B + + def pre_sequence(self): + self.b = B.deploy() + + # Flows test all external functions that are not marked as view or pure + @flow() + def flow_foo(self): + assert self.b.foo() == 1 + + # One flow per external function + @flow() + def flow_bar(self): + assert self.b.bar() == 2 +``` \ No newline at end of file diff --git a/docs/cookbook/essential-fundamentals/index.md b/docs/cookbook/essential-fundamentals/index.md new file mode 100644 index 000000000..2a939345a --- /dev/null +++ b/docs/cookbook/essential-fundamentals/index.md @@ -0,0 +1,6 @@ +# Essential Fundamentals +Core patterns and structures that form the foundation of Wake fuzz testing. + +- [Basic Fuzz Test Structure](basic-fuzz-test-structure.md) +- [Error Handling](error-handling.md) +- [Flows](flows.md) diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md new file mode 100644 index 000000000..bae87db05 --- /dev/null +++ b/docs/cookbook/index.md @@ -0,0 +1,14 @@ +# Wake Cookbook + +Welcome to the Wake Cookbook - a guide to fuzz testing patterns for smart contracts using Wake. This cookbook is organized into categories from fundamental to specialized patterns. + +!!! warning "Important" + This cookbook is a work in progress. While it may provide you with some useful patterns, it may not cover all the use cases. + +## Categories + +- [Essential Fundamentals](essential-fundamentals/index.md) +- [Common Testing Patterns](common-testing-patterns/index.md) +- [Advanced Testing Features](advanced-testing-features/index.md) +- [Specialized Use Cases](specialized-use-cases/index.md) +- [Testing Infrastructure](testing-infrastructure/index.md) diff --git a/docs/cookbook/specialized-use-cases/address-bytes-conversion-with-random-data.md b/docs/cookbook/specialized-use-cases/address-bytes-conversion-with-random-data.md new file mode 100644 index 000000000..e5419ef30 --- /dev/null +++ b/docs/cookbook/specialized-use-cases/address-bytes-conversion-with-random-data.md @@ -0,0 +1,29 @@ +# Address/Bytes Conversion Testing + +Conversion between addresses and bytes with random data, including validation of byte length and error handling. + +```solidity +library AddressBytesUtils { + function toAddress(bytes memory b) internal pure returns (address) { + if (b.length != 20) { + revert("Invalid length"); + } + assembly { addr := mload(add(b, 20)) } + } +} +``` + +```python +class AddressConversionFuzzTest(FuzzTest): + @flow() + def flow_to_address(self) -> None: + length = random_int(0, 20) + b = random_bytes(length) + + with may_revert("Invalid length") as e: + a = self.utils.toAddress(b) + assert a == Address(b.hex()) + + if e is not None: + assert e.value == "Invalid length" +``` diff --git a/docs/cookbook/specialized-use-cases/cross-chain-message-passing.md b/docs/cookbook/specialized-use-cases/cross-chain-message-passing.md new file mode 100644 index 000000000..2c6ae3139 --- /dev/null +++ b/docs/cookbook/specialized-use-cases/cross-chain-message-passing.md @@ -0,0 +1,35 @@ +# Cross-Chain Message Passing + +Example of testing cross-chain message passing between two chains. + +```python +class CrossChainFuzzTest(FuzzTest): + def pre_sequence(self) -> None: + self.chain1 = Chain() + self.chain2 = Chain() + self.service1 = Service.deploy(chain=self.chain1) + self.service2 = Service.deploy(chain=self.chain2) + + @flow() + def flow_cross_chain_send(self) -> None: + amount = random_int(0, 2**256 - 1) + sender = random_account(chain=self.chain1) + recipient = random_account(chain=self.chain2) + + # Send on source chain + tx1 = self.service1.sendMessage( + "chain2", + recipient.address, + amount, + from_=sender + ) + + # Execute on destination chain + self.service2.executeMessage( + "chain1", + sender.address, + amount, + tx1.events[0].messageHash, + from_=random_account(chain=self.chain2) + ) +``` \ No newline at end of file diff --git a/docs/cookbook/specialized-use-cases/deploy-with-proxy.md b/docs/cookbook/specialized-use-cases/deploy-with-proxy.md new file mode 100644 index 000000000..fa036a423 --- /dev/null +++ b/docs/cookbook/specialized-use-cases/deploy-with-proxy.md @@ -0,0 +1,19 @@ +# Deploy with Proxy + +Example of deploying a contract with a proxy. + +```solidity +contract Proxy is ERC1967Proxy { + constructor(address implementation, bytes memory data) ERC1967Proxy(implementation, data) { ... } +} + +contract MyContract { + function initialize(address owner, uint256 arg) public { ... } +} +``` + +```python +impl = MyContract.deploy() +proxy = Proxy.deploy(impl, abi.encode(self.owner, 1)) +self.my_contract = MyContract(proxy) +``` diff --git a/docs/cookbook/specialized-use-cases/index.md b/docs/cookbook/specialized-use-cases/index.md new file mode 100644 index 000000000..d30b3edc1 --- /dev/null +++ b/docs/cookbook/specialized-use-cases/index.md @@ -0,0 +1,9 @@ +# Specialized Use Cases +Testing patterns designed for specific types of smart contract architectures like cross-chain bridges, upgradeable contracts, and multi-chain deployments. Reference these when working with specialized contract features. + +- [Address Bytes Conversion with Random Data](address-bytes-conversion-with-random-data.md) +- [Cross Chain Message Passing](cross-chain-message-passing.md) +- [Deploy with Proxy](deploy-with-proxy.md) +- [Multi Chain Token Deployments](multi-chain-token-deployments.md) +- [Multi Token Accounting](multi-token-accounting.md) +- [Permit Functions with EIP712 Signatures](permit-functions-with-eip712-signatures.md) diff --git a/docs/cookbook/specialized-use-cases/multi-chain-token-deployments.md b/docs/cookbook/specialized-use-cases/multi-chain-token-deployments.md new file mode 100644 index 000000000..e26cc84f1 --- /dev/null +++ b/docs/cookbook/specialized-use-cases/multi-chain-token-deployments.md @@ -0,0 +1,33 @@ +# Multi-Chain Token Deployments + +Example of deploying contracts on multiple chains. + +```python +@flow() +def flow_deploy_remote_tokens(self) -> None: + num_chains = random_int(1, 5) + chains = [f"chain{i}" for i in range(num_chains)] + gas_values = [random_int(1000, 10000) for _ in range(num_chains)] + mgr_types = [random_int(0, 3) for _ in range(num_chains)] + + params = [] + for i in range(num_chains): + if mgr_types[i] == 2: # Canonical + params.append(self._get_canonical_params()) + else: + params.append(self._get_standard_params()) + + with may_revert() as e: + self.service.deployRemoteCustomTokenManagers( + random_bytes(32), # salt + chains, + mgr_types, + params, + gas_values, + value=sum(gas_values), + from_=random_account() + ) + + if len(set(chains)) != len(chains): + assert e.value == self.service.DuplicateChain() +``` \ No newline at end of file diff --git a/docs/cookbook/specialized-use-cases/multi-token-accounting.md b/docs/cookbook/specialized-use-cases/multi-token-accounting.md new file mode 100644 index 000000000..78778a238 --- /dev/null +++ b/docs/cookbook/specialized-use-cases/multi-token-accounting.md @@ -0,0 +1,22 @@ +# Multi-Token Accounting + +Example of testing a contract that tracks multiple tokens and users. + +```python +class TokenAccounting: + balances: dict[Address, dict[Address, int]] # token -> user -> amount + + def track_token(self, token: Address, user: Address, amount: int): + if token not in self.balances: + self.balances[token] = {} + if user not in self.balances[token]: + self.balances[token][user] = 0 + + self.balances[token][user] += amount + + @invariant() + def invariant_token_accounting(self): + for token, users in self.balances.items(): + for user, amount in users.items(): + assert self.tokens[token].balanceOf(user) == amount +``` \ No newline at end of file diff --git a/docs/cookbook/specialized-use-cases/permit-functions-with-eip712-signatures.md b/docs/cookbook/specialized-use-cases/permit-functions-with-eip712-signatures.md new file mode 100644 index 000000000..2e04534ce --- /dev/null +++ b/docs/cookbook/specialized-use-cases/permit-functions-with-eip712-signatures.md @@ -0,0 +1,45 @@ +# Permit Functions with EIP712 Signatures + +Example of testing a permit function with EIP712 signatures. +```python +@dataclass +class Permit: + owner: Address + spender: Address + value: uint256 + nonce: uint256 + deadline: uint256 + +@flow() +def flow_permit(self) -> None: + owner = random_account() + spender = random_account() + value = random_int(0, 2**256 - 1) + + permit = Permit( + owner.address, + spender.address, + value, + self.token.nonces(owner), + self.token.chain.blocks["latest"].timestamp + 100_000 + ) + + signature = owner.sign_structured(permit, Eip712Domain( + name=self.token.name(), + version="1", + chainId=self.token.chain.chain_id, + verifyingContract=self.token.address, + )) + + with may_revert() as e: + self.token.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + signature[64], # v + signature[:32], # r + signature[32:64], # s + from_=random_account() + ) +``` \ No newline at end of file diff --git a/docs/cookbook/testing-infrastructure/index.md b/docs/cookbook/testing-infrastructure/index.md new file mode 100644 index 000000000..93dd1b9ee --- /dev/null +++ b/docs/cookbook/testing-infrastructure/index.md @@ -0,0 +1,7 @@ +# Testing Infrastructure +Supporting patterns and utilities that help organize, maintain, and improve the quality of test suites. These patterns focus on test organization, logging, and cleanup operations. + +- [Initialization Strategies](initialization-strategies.md) +- [Logging with Formatting](logging-with-formatting.md) +- [Post Sequence Cleanup](post-sequence-cleanup.md) +- [Results Collection](results-collection.md) diff --git a/docs/cookbook/testing-infrastructure/initialization-strategies.md b/docs/cookbook/testing-infrastructure/initialization-strategies.md new file mode 100644 index 000000000..6f75a4bf2 --- /dev/null +++ b/docs/cookbook/testing-infrastructure/initialization-strategies.md @@ -0,0 +1,93 @@ +# Initialization Strategies + +Example of testing different initialization strategies for a factory contract. + +```solidity +contract Factory { + enum StrategyType { BASIC, ADVANCED, UPGRADEABLE, PROXY } + + error InvalidStrategy(); + error InvalidParams(); + + struct BasicParams { + address admin; + uint256 value; + } + + struct AdvancedParams { + address admin; + string name; + uint8 version; + uint256 config; + } + + struct UpgradeableParams { + address admin; + address implementation; + bytes initData; + } + + struct ProxyParams { + address admin; + address logic; + address proxy; + } + + function deploy( + bytes32 salt, + StrategyType strategyType, + bytes memory params + ) external returns (address) { + // Deploy contract based on strategy + } +} +``` + +```python +@flow() +def flow_deploy_with_strategy(self) -> None: + # Test different initialization strategies + strategy_type = random_int(0, 3) + admin = random_account() + + # Generate params based on strategy + params = { + 0: self._encode_basic_params( + admin=admin.address, + value=random_int(0, 1000) + ), + 1: self._encode_advanced_params( + admin=admin.address, + name=random_string(10), + version=random_int(1, 5), + config=random_int(0, 1000) + ), + 2: self._encode_upgradeable_params( + admin=admin.address, + implementation=random_address(), + init_data=random_bytes(32) + ), + 3: self._encode_proxy_params( + admin=admin.address, + logic=random_address(), + proxy=random_address() + ) + } + + salt = random_bytes(32) + + with may_revert(Factory.InvalidStrategy) as e: + self.factory.deploy( + salt, + strategy_type, + params[strategy_type], + from_=admin + ) + + if strategy_type > 3: + assert e.value == Factory.InvalidStrategy() + else: + assert e.value is None + deployed = self.factory.getDeployed(salt) + self._validate_deployment(deployed, strategy_type, params[strategy_type]) +``` diff --git a/docs/cookbook/testing-infrastructure/logging-with-formatting.md b/docs/cookbook/testing-infrastructure/logging-with-formatting.md new file mode 100644 index 000000000..4a584831a --- /dev/null +++ b/docs/cookbook/testing-infrastructure/logging-with-formatting.md @@ -0,0 +1,20 @@ +# Logging with Formatting + +Example of logging with formatting in a fuzz test. + +```python +class FormattedLoggingTest(FuzzTest): + def amount_str(self, amount: int) -> str: + return str(amount / 10**18) # Format with decimals + + @flow() + def flow_with_logging(self): + amount = random_amount() + user = random_account() + + before = self.contract.balanceOf(user) + self.contract.deposit(amount, from_=user) + after = self.contract.balanceOf(user) + + log(f"[green]{user} deposits {self.amount_str(after - before)}[/]") +``` \ No newline at end of file diff --git a/docs/cookbook/testing-infrastructure/post-sequence-cleanup.md b/docs/cookbook/testing-infrastructure/post-sequence-cleanup.md new file mode 100644 index 000000000..56e7e715f --- /dev/null +++ b/docs/cookbook/testing-infrastructure/post-sequence-cleanup.md @@ -0,0 +1,20 @@ +# Post-Sequence Cleanup + +Example of testing a contract with a post-sequence cleanup function. + +```python +class CleanupTest(FuzzTest): + def post_sequence(self): + # Clean up remaining balances + for account in self.active_accounts: + if self.contract.balanceOf(account) > 0: + if random_bool(): + self.withdraw(account, self.contract.balanceOf(account)) + + if self.contract.claimable(account) > 0: + if random_bool(): + self.claim(account) + + # Verify final state + self.verify_final_state() +``` \ No newline at end of file diff --git a/docs/cookbook/testing-infrastructure/results-collection.md b/docs/cookbook/testing-infrastructure/results-collection.md new file mode 100644 index 000000000..776e3fb91 --- /dev/null +++ b/docs/cookbook/testing-infrastructure/results-collection.md @@ -0,0 +1,21 @@ +# Results Collection + +Example of collecting and logging results from a contract. + +```python +class TestResults: + total_supply: int + total_claimed: int + balances: dict[Address, int] + + def collect_state(self, contract) -> None: + self.total_supply = contract.totalSupply() + self.total_claimed = contract.totalClaimed() + +class ResultTrackingTest(FuzzTest): + results: TestResults + + def post_sequence(self): + self.results.collect_state(self.contract) + self.log_results() +``` diff --git a/mkdocs.yml b/mkdocs.yml index cfe5b57e6..7825fba9f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -220,6 +220,40 @@ nav: - variable_declaration: 'api-reference/ir/yul/variable-declaration.md' - wake.printers: - api: 'api-reference/printers/api.md' + - Cookbook: + - 'cookbook/index.md' + - Essential Fundamentals: + - 'cookbook/essential-fundamentals/index.md' + - Basic Fuzz Test Structure: 'cookbook/essential-fundamentals/basic-fuzz-test-structure.md' + - Error Handling: 'cookbook/essential-fundamentals/error-handling.md' + - Flows: 'cookbook/essential-fundamentals/flows.md' + - Common Testing Patterns: + - 'cookbook/common-testing-patterns/index.md' + - Account Balance Testing: 'cookbook/common-testing-patterns/account-balance-testing.md' + - Multi Token Interaction: 'cookbook/common-testing-patterns/multi-token-interaction.md' + - State Change Tracking: 'cookbook/common-testing-patterns/state-change-tracking.md' + - Test Flow Branching: 'cookbook/common-testing-patterns/test-flow-branching.md' + - Advanced Testing Features: + - 'cookbook/advanced-testing-features/index.md' + - Context Based Balance Tracking Pattern: 'cookbook/advanced-testing-features/context-based-balance-tracking-pattern.md' + - Differential Testing: 'cookbook/advanced-testing-features/differential-testing.md' + - Error Tolerance: 'cookbook/advanced-testing-features/error-tolerance.md' + - Time Based Testing: 'cookbook/advanced-testing-features/time-based-testing.md' + - Token Allowances with Multiple Branches: 'cookbook/advanced-testing-features/token-allowances-with-multiple-branches.md' + - Specialized Use Cases: + - 'cookbook/specialized-use-cases/index.md' + - Address Bytes Conversion with Random Data: 'cookbook/specialized-use-cases/address-bytes-conversion-with-random-data.md' + - Cross Chain Message Passing: 'cookbook/specialized-use-cases/cross-chain-message-passing.md' + - Deploy with Proxy: 'cookbook/specialized-use-cases/deploy-with-proxy.md' + - Multi Chain Token Deployments: 'cookbook/specialized-use-cases/multi-chain-token-deployments.md' + - Multi Token Accounting: 'cookbook/specialized-use-cases/multi-token-accounting.md' + - Permit Functions with EIP712 Signatures: 'cookbook/specialized-use-cases/permit-functions-with-eip712-signatures.md' + - Testing Infrastructure: + - 'cookbook/testing-infrastructure/index.md' + - Initialization Strategies: 'cookbook/testing-infrastructure/initialization-strategies.md' + - Logging with Formatting: 'cookbook/testing-infrastructure/logging-with-formatting.md' + - Post Sequence Cleanup: 'cookbook/testing-infrastructure/post-sequence-cleanup.md' + - Results Collection: 'cookbook/testing-infrastructure/results-collection.md' extra: generator: false