introduction
Writing comprehensive fuzz tests for Solidity contracts is necessary but time consuming. Complex state machines have so many execution paths and smart contract states that manual code review cannot catch all vulnerabilities.
Vibe Coding – Using AI to generate boilerplate code while putting humans in charge of the logic provides a better workflow. Testing smart contracts in Python using Wake supports this approach because switching languages requires you to think about behavior rather than syntax.
This guide defines the boundaries between what AI can safely generate and what always requires human confirmation in manually guided fuzzing tests.
Look at the structure and check the logic.
MGF involves writing a lot of repetitive code, primarily importing pytypes, Python state definitions, and static logic.
A Python for-loop that iterates over accounts can be coded in Vibe. If-elif control structures can be Vibe coded. Error message strings can be vibe coded. The structural element of the test is mechanical work.
However, the three components require manual verification every time.
Verification Checklist
- What does the test claim? These are the correct properties to check. All event parameters are checked, as well as event existence. All error conditions are mapped to assertions. Invariance expresses a true mathematical or logical property.
- What values are they asserting? The value is compared to the Python state calculated before the transaction. Error parameters are fully checked. State changes reflect contract behavior, including extreme cases such as fees and rounding.
- Execution branch reached: Use Wake’s coverage reports and manually guided fuzzing output to ensure that your intended code paths are executed and properly tested.
If any of these three are missing, the test may seem perfect, but it won’t catch anything.
4 steps for every flow function
All flow functions in Wake follow the same pattern. Understanding which steps can be Vibe coded and which require manual verification determines testing effectiveness.
@flow()
def flow_something(self):
## Phase 1: prepare random input
## Phase 2: run transaction
## Phase 3: check events
## Phase 4: update python state
passStep 1: Prepare random input
The Vibe code codes random selection structures such as selecting an account, generating an amount, or selecting from a collection. AI effectively handles these mechanical tasks.
Specify constraints manually. Business logic determines valid scopes, prerequisites, and parameter relationships. This encodes domain knowledge that AI lacks.
Step 2: Execute transaction
The vibe code is may_revert() Wrapper structure. This is standard Python error handling.
Manually specify the exact parameters passed in the Python state. Transaction parameters vary depending on the test scenario and current state.
Step 3: Confirm the event
The most common testing errors occur at this stage. You can Vibe code an event filtering loop structure, but you have to manually check the assertions.
Most importantly, event validation should only rely on Python state variables computed before the transaction.
Never use the following:
if len(tx.events) > 0:
assert events(0).amount == expected_amountIt’s about checking what’s wrong. Instead, check all parameters deterministically.
events = (e for e in tx.events if isinstance(e, Contract.Transfer))
assert len(events) == 1
assert events(0).sender == sender
assert events(0).recipient == recipient
assert events(0).amount == expected_amountAll parameters are checked against pre-calculated states. AI creates the first version. We’ll have to see if it comes second.
Step 4: Update Python State
Vibe codes assignment structures, which are the basic syntax for updating dictionaries and variables.
Manually check which values are updated. Fees, rounding, extreme cases, etc. all reflect our understanding of how the contract works. Because immutable functions rely on accurate state tracking, errors will cascade through all subsequent tests.
Handling reverts with Python model verification
When a transaction is reverted, thorough error mapping is required. You can vibe code if-elif control structures and error message strings. You have to manually verify what it claims and which execution branch it reached.
with may_revert() as e:
tx = self.contract.transfer(recipient, amount, from_=sender)
if self.balances(sender) < amount:
assert e.value is not None
assert isinstance(e.value, Contract.InsufficientBalance)
assert e.value.required == amount
assert e.value.available == self.balances(sender)
return "insufficient_balance"
elif recipient == Address.ZERO:
assert e.value is not None
assert isinstance(e.value, Contract.InvalidRecipient)
return "zero_address"
assert e.value is NonePython state design
Subtle mistakes reduce testing effectiveness. Many contracts use special values such as: address(0) Represent the native token and create a branch in the contract code. Please do not duplicate these implementation details in Python state.
You can vibe code the for loop structure, but check what you’re claiming.
Invalid version reflecting contract implementation:
@invariant()
def invariant_token_balances(self):
for token in self.tokens + (Address.ZERO):
for account in self.all_accounts:
if token != Address.ZERO:
assert self.token_balances(token)(account) == IERC20(token).balanceOf(account)
else:
assert self.token_balances(token)(account) == account.balanceA better version using natural semantic expressions:
@invariant()
def invariant_erc20_balances(self):
for token in self.erc20_tokens:
for account in self.all_accounts:
assert self.erc20_balances(token)(account) == token.balanceOf(account)
@invariant()
def invariant_native_balances(self):
for account in self.all_accounts:
assert self.native_balances(account) == account.balanceThe second version separates the semantically different ones. The test code reflects what the token actually is, not how the contract implements the token. This independent model catches bugs instead of hiding them.
Consider whether you are verifying or reproducing the contract logic. If your test has the same conditional branch as the contract, the bug will be missed.
development cycle
Start with the simplest state change function in your contract. It is the building block on which other operations depend.
Vibe codes flow structures. Flow function name, decorator, outline, transaction call and population parameters. We then manually check the important parts, selecting parameters from the state, processing full reverts, thorough event validation, correct state updates and their logic.
Write immutable functions that check Python state and contract state. Vibe code functional structures and loops. Manually verify what the assertion confirms.
Run fuzzing and check coverage. Find untested branches. Make sure that branch is meaningful and not dead code. Each iteration shows how the contract behaves in extreme cases that were not considered.
Real-world example: Token transfer flow
This complete example demonstrates the token transfer functionality.
@flow()
def flow_transfer_tokens(self):
# Phase 1: Prepare random input (vibe code structure, manually verify constraints)
sender = random_account()
recipient = random_account()
amount = random_int(0, 10**30)
# Phase 2: Run transaction (vibe code wrapper, manually specify parameters)
with may_revert() as e:
tx = self.token.transfer(recipient, amount, from_=sender)
# Phase 3: Check events (vibe code filtering, manually verify all parameters)
if self.balances(sender) < amount:
assert e.value is not None
assert isinstance(e.value, Token.InsufficientBalance)
assert e.value.required == amount
assert e.value.available == self.balances(sender)
return "insufficient_balance"
assert e.value is None
events = (e for e in tx.events if isinstance(e, Token.Transfer))
assert len(events) == 1
assert events(0).from_ == sender.address
assert events(0).to == recipient.address
assert events(0).value == amount
# Phase 4: Update Python state (vibe code structure, manually verify values)
self.balances(sender) -= amount
self.balances(recipient) += amountAI generates structural patterns while you check every assertion, every value, and every execution branch.
conclusion
Testing smart contracts in Python using Wake enables a Vive coding workflow that accelerates test development while maintaining security.
Vibe codes mechanical parts such as pytype, for-loop, if-elif structures, error strings, function signatures, and import type hints.
Always manually verify what you assert, what value it asserts, and what execution branch it reached. These three verification points hide security vulnerabilities.
The boundary between what you want to vibe your code with and what you want to verify will determine whether you write comprehensive fuzz tests efficiently or introduce hidden vulnerabilities in the generated code that appear correctly.
Master this trade-off to strengthen your smart contract security testing.
Additional Resources
Wake Test Framework Documentation: Wake Fuzzing Guide
For more fuzzing techniques and best practices, follow @WakeFramework on X.
