introduction
The manual fuzzy (MGF) is a test methodology that finds important vulnerabilities by systematically testing smart contract movements through guide test scenarios.
Unlike traditional fuzzy, which depends on random satellites, the developer defines a specific test flow and immutable amount to provide more target and effective vulnerability detection.
Learn MGF to strengthen smart contract security.
Comprehensive Document: Wake Test Framework -Fujing
Comparison with FOUNDRY Puz Test and Constant Test
The most important factor in all wise contract tests is to define the clear immutability that must always maintain the facts regardless of the change of the contract.
WAKE compares the actual contract operation and the expected behavior (defined by Python) without relying on the internal logic of the contract.
This methodology guarantees an edge case where the tester confirms behavior at all stages, guarantees a comprehensive range, and misses other test approaches.
Wake up the MGF Life Cycle
Wake MGF follows other execution life cycles compared to Foundry’s fuzz tests or unchanging tests.
Wake MGF Run Life Cycle:
where:
flow_count
Define the number of flow function calls that have been executed after each.pre_sequence
Function (initialization of contract)sequence_count
Define the number of complete test sequences to run
Each sequence consists of one pre_sequence
The numbered number is followed flow
Function call.
Learn more about Wake’s execution hooks.
Implementation Guide
Prerequisites
- Wake framework installed on the system
- Understanding the basics of Python and rigidity
- Robberable projects prepared to test or use the code in the appendix.
Full source code
Full source code that can be used in the appendix.
1. Compile the project with Wake
- run
$ wake up
To compile a solid agreement and create a Python type definition - Pytypes is automatically created
pytypes
directory - Create test file:
tests/test_fuzz.py
Pytypes provides a Python interface for a robust agreement, enabling types-safety interactions during testing.
Action: Set the project structure using the test file in the correct position.
2. Bring wake
Wake test income.
from wake.testing import *
from wake.testing.fuzzing import *
3. Import Pytypes
Look at the Pytypes directory and get the contract Pytypes and get the pytypes.
from pytypes.contracts.Token import Token
4. Define the test class and call in the test.
Purging Base Class FuzzTest
It is defined wake.testing.fuzzing
.
from wake.testing import *
from wake.testing.fuzzing import *
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
from pytypes.contracts.Token import Token
def revert_handler(e: RevertError):
if e.tx is not None:
print(e.tx.call_trace)
class TokenFuzz(FuzzTest):
def pre_sequence(self):
pass
@flow()
def flow_example(self):
pass
@invariant()
def invariant_example(self):
pass
@chain.connect()
@on_revert(revert_handler)
def test_default():
TokenFuzz.run(sequences_count=1, flows_count=100)
The next section is TokenFuzz
class.
5. Define the initialization of the contract and Python status
that pre_sequence
The function acts as a setting step of each test sequence.
- Contract: Initialize the contract you want to test
- Define the actor: Set account to interact with the contract
- Initialize the Python state: Create a data structure for tracking the expected contract status
This separation allows each test sequence to start with a clean and known state.
class TokenFuzz(FuzzTest):
token_owner: Account
token: Token
token_balances: dict(Account, int)
def pre_sequence(self):
self.token_owner = random_account()
self.token = Token.deploy(from_=self.token_owner)
self.token_balances = defaultdict(int)
6. Flow definition
What is the flow function?
The flow function is the core of the MGF test. Each flow function creates test input, runs contract calls, verifies the operation, and updates the python state to mirror.
The flow function systematically tests other input combinations and execution paths to simulate the actual usage pattern and edge case.
This is the flow function.
@flow()
def flow_mint_tokens(self):
##1. prepare random input
recipient = random_account() # or random.choice(list(chain.accounts) + (self.token))
amount = random_int(0, 10**30)
actor = random_account()
##2. run transaction
with may_revert() as e:
tx = self.token.mintTokens(recipient, amount, from_=actor)
if e.value is not None:
## 3. check revert
if actor != self.token_owner:
assert e.value == Token.NotAuthorized(actor.address)
return "Not authorized"
assert False
##4. check events
events = (e for e in tx.events if isinstance(e, Token.TokensMinted))
assert len(events) == 1
assert events(0).to == recipient.address
assert events(0).amount == amount
##5. update python state
self.token_balances(recipient) += amount
##6. logging for debug
logger.info(f"Minted amount tokens to recipient.address")
In all flow functions, follow this structured approach.
- Prepare any input
- When you turn it back, run the transaction
- Validate the case and claim
- Update the Python status
- Add debugging logging (if necessary)
Step 1: Preparation of arbitrary input
Create a test input such as the built -in random function of Wake random_account()
,,, random_int(min, max)
and random_bytes(length)
. These features ensure a comprehensive test range in various input scenarios.
Full document: https://ackee.xyz/wake/docs/latest/testing-fuzzing/#random-funtions
Step 2: Run the transaction with reverse processing
Use may_revert()
The context manager handles both successful and failed transactions. This enables branch logic for success/failure cases. use assert False
Capture unexpected reverse conditions and return the expected narrative string to track the test statistics.
with may_revert() as e:
tx = self.token.mintTokens(recipient, amount, from_=actor)
if e.value is not None:
if condition:
# assert e.value == RevertError()
return "Reason"
elif other_condition:
# assert e.value == RevertOtherError()
return "OtherReason"
assert False
Step 3: Check out events and arguments
Always check the error.
Always check the events of the test target.
Events and ribers can be found in the following way.
events = (e for e in tx.events if e == Token.TokensMinted(recipient.address, amount))
assert len(events) == 1
Or claim filters and parameters for each event
events = (e for e in tx.events if isinstance(e, Token.TokensMinted))
assert len(events) == 1
assert events(0).to == recipient.address
assert events(0).amount == amount
that isinstance()
Approaches are recommended in complex verification scenarios such as transactions that emit multiple events. This method provides accurate error reports to accurately show the parameters that have failed in the assault.
Step 4: Python State Update
Reflects the contract status change of Python variables. This parallel state tracking enables accurate changes. Do not derive status updates in View Functions. Always update based on the known effects of transactions.
Constant function
The immutable function checks for important properties throughout the contract execution. The Python state is compared with the actual contract state using the VIEW function.
For complex protocols, immutability may include sophisticated logic to verify multiple contract interactions. Do not modify the status within the immutable function. For validation, use it if you need a status change operation. snapshot_and_revert()
It does not affect the test sequence.
MGF’s immutable definition
Check out the view function in Python State.
Check out the constant statement and conditional immutability.
every @invariant()
The function is called after each @flow
Function call.
In these features, the state does not change.
@invariant()
def invariant_token_balances(self):
for account in list(self.token_balances.keys()) + (self.token):
assert self.token.getBalance(account.address) == self.token_balances(account)
@invariant()
def invariant_token_owner(self):
assert self.token.owner() == self.token_owner.address
Test execution
Execution:
$ wake test tests/test_token_fuzz.py
This is a small run of Flow_number.
If the test fails, use the debug mode.
$ wake test tests/test_token_fuzz.py -d
Run shows any seeds of seizure. You can use this hexadecimal to reproduce the same test, including failure.
Set a specific seed for a reproducible test.
$ wake test tests/test_token_fuzz.py -S 235ab3
The Fuzzy Tips and Professional Methodology: Follow @wakeframework in X.
conclusion
The manually induced pursing provides a systematic approach that provides a contract logic and a deep insight into the edge case.
Appendix -All code
TOKEN.SOL
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Token
address public immutable owner;
mapping(address => uint256) public tokenBalance;
event Transfer(address indexed from, address indexed to, uint256 value);
event TokensMinted(address indexed to, uint256 amount);
error NotEnoughTokens(uint256 requested, uint256 balance);
error NotAuthorized(address caller);
constructor()
owner = msg.sender;
modifier onlyOwner()
if (msg.sender != owner)
revert NotAuthorized(msg.sender);
_;
function mintTokens(address recipient, uint256 amount) external onlyOwner
tokenBalance(recipient) += amount;
emit TokensMinted(recipient, amount);
function transfer(address to, uint256 amount) external
if (tokenBalance(msg.sender) < amount)
revert NotEnoughTokens(amount, tokenBalance(msg.sender));
tokenBalance(msg.sender) -= amount;
tokenBalance(to) += amount;
emit Transfer(msg.sender, to, amount);
function transferWithBytes(bytes calldata data) external
(address to, uint256 amount) = abi.decode(data, (address, uint256));
if (tokenBalance(msg.sender) < amount)
revert NotEnoughTokens(amount, tokenBalance(msg.sender));
tokenBalance(msg.sender) -= amount;
tokenBalance(to) += amount;
emit Transfer(msg.sender, to, amount);
function getBalance(address account) external view returns (uint256)
return tokenBalance(account);
test_token_fuzz.py
from wake.testing import *
from collections import defaultdict
from wake.testing.fuzzing import *
from pytypes.contracts.Token import Token
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# Print failing tx call trace
def revert_handler(e: RevertError):
if e.tx is not None:
print(e.tx.call_trace)
class TokenFuzz(FuzzTest):
token_owner: Account
token: Token
token_balances: dict(Account, int)
def pre_sequence(self):
self.token_owner = random_account()
self.token = Token.deploy(from_=self.token_owner)
self.token_balances = defaultdict(int)
@flow()
def flow_mint_tokens(self):
## prepare random input
recipient = random_account() # or list(chain.accounts) + (self.token)
amount = random_int(0, 10**30)
actor = random_account()
## run transaction
with may_revert() as e:
tx = self.token.mintTokens(recipient.address, amount, from_=actor)
if e.value is not None:
if actor != self.token_owner:
assert e.value == Token.NotAuthorized(actor.address)
return "Not authorized"
assert False
## check events
events = (e for e in tx.events if isinstance(e, Token.TokensMinted))
assert len(events) == 1
assert events(0).to == recipient.address
assert events(0).amount == amount
## update python state
self.token_balances(recipient) += amount
logger.info(f"Minted amount tokens to recipient.address")
@flow()
def flow_transfer_tokens(self):
recipient = random_account()
amount = random_int(0, 10**30)
actor = random_account()
with may_revert() as e:
tx = self.token.transfer(recipient.address, amount, from_=actor)
if e.value is not None:
if self.token_balances(actor) < amount:
assert e.value == Token.NotEnoughTokens(amount, self.token_balances(actor))
return "Not enough tokens"
assert False
events = (e for e in tx.events if isinstance(e, Token.Transfer))
assert len(events) == 1
assert events(0).from_ == actor.address
assert events(0).to == recipient.address
assert events(0).value == amount
self.token_balances(recipient) += amount
self.token_balances(actor) -= amount
logger.info(f"Transferred amount tokens from actor.address to recipient.address")
@flow()
def flow_transfer_tokens_with_bytes(self):
recipient = random_account()
amount = random_int(0, 10**30)
actor = random_account()
with may_revert() as e:
tx = self.token.transferWithBytes(abi.encode(recipient.address, uint256(amount)), from_=actor)
if e.value is not None:
if self.token_balances(actor) < amount:
assert e.value == Token.NotEnoughTokens(amount, self.token_balances(actor))
return "Not enough tokens"
assert False
events = (e for e in tx.events if isinstance(e, Token.Transfer))
assert len(events) == 1
assert events(0).from_ == actor.address
assert events(0).to == recipient.address
assert events(0).value == amount
self.token_balances(recipient) += amount
self.token_balances(actor) -= amount
logger.info(f"Transferred amount tokens from actor.address to recipient.address")
@invariant()
def invariant_token_balances(self):
for account in list(self.token_balances.keys()) + (self.token):
assert self.token.getBalance(account.address) == self.token_balances(account)
@invariant()
def invariant_token_owner(self):
assert self.token.owner() == self.token_owner.address
@chain.connect()
def test_default():
TokenFuzz.run(sequences_count=10, flows_count=10000)