Fuzz Testing with Medusa¶
Medusa is a powerful fuzzing framework that tests smart contract invariants and properties by generating random inputs to discover edge cases and unexpected behaviors.
What Fuzz Testing Tests For¶
Medusa validates contracts through property-based testing:
Invariant Testing¶
- State Invariants: Properties that must always hold true
- Balance Consistency: Token balances remain consistent
- Access Control: Permissions are never violated
- Numerical Bounds: Values stay within expected ranges
- Relationship Preservation: Related values maintain their relationships
Edge Case Discovery¶
- Boundary Values: Tests with extreme values (0, max uint, etc.)
- Unexpected Inputs: Random combinations that might be overlooked
- State Transitions: Complex sequences of operations
- Race Conditions: Concurrent operation interactions
Property Violations¶
- Assertion Failures: require/assert statements that can be broken
- Panic Conditions: Solidity panics (overflow, divide by zero, etc.)
- Optimization Issues: Patterns that could be gas-optimized
- Custom Properties: User-defined invariants
How Fuzzing Works¶
Medusa uses intelligent fuzzing techniques:
- Random Input Generation: Creates random transaction sequences
- Corpus-Based Fuzzing: Learns from successful test cases
- Coverage Guidance: Prioritizes inputs that increase code coverage
- Mutation Strategies: Modifies inputs to find new behaviors
- Property Checking: Validates invariants after each transaction
Example¶
// Invariant: Total supply should never exceed initial supply
function property_total_supply_bounded() public view returns (bool) {
return registry.totalSupply() <= INITIAL_SUPPLY;
}
Medusa will: - Generate random transaction sequences - Execute them against the contract - Check if the property still holds - Report any sequence that violates it
Installation¶
Prerequisites¶
Install Medusa¶
Install Dependencies¶
Verify Installation¶
Configuration¶
Medusa is configured via medusa.json:
{
"fuzzing": {
"workers": 10,
"workerResetLimit": 50,
"timeout": 0,
"testLimit": 0,
"callSequenceLength": 100,
"corpusDirectory": "medusa-corpus",
"coverageEnabled": true,
"targetContracts": [
"ProposalRegistryFuzzTest",
"WelfareMetricRegistryFuzzTest"
],
"testing": {
"stopOnFailedTest": true,
"assertionTesting": {
"enabled": true,
"testViewMethods": false
},
"propertyTesting": {
"enabled": true,
"testPrefixes": ["property_"]
},
"optimizationTesting": {
"enabled": true,
"testPrefixes": ["optimize_"]
}
}
}
}
Key Configuration Options¶
- workers: Number of parallel fuzzing workers (default: 10)
- callSequenceLength: Maximum transaction sequence length (default: 100)
- corpusDirectory: Where to save interesting test cases
- coverageEnabled: Track and optimize for code coverage
- targetContracts: Contracts to fuzz test
Writing Fuzz Tests¶
Test Contract Structure¶
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.24;
import "../../contracts/ProposalRegistry.sol";
contract ProposalRegistryFuzzTest {
ProposalRegistry public registry;
constructor() {
registry = new ProposalRegistry();
}
// Property test: must start with "property_"
function property_proposal_count_never_decreases() public view returns (bool) {
uint256 count = registry.proposalCount();
return count >= 0; // Always true, but validates access
}
// Property test with parameters
function property_bond_sufficient(uint256 amount) public view returns (bool) {
return amount >= registry.bondAmount();
}
}
Property Functions¶
Property functions must:
- Start with property_ prefix
- Return bool (true = pass, false = fail)
- Be public or external
- Can be view or pure for state checking
- Can accept parameters for input fuzzing
Test Types¶
1. Invariant Properties¶
Test conditions that must always hold:
function property_total_weight_bounded() public view returns (bool) {
return registry.totalActiveWeight() <= 10000; // Max 100%
}
2. Relationship Properties¶
Test relationships between values:
function property_balance_consistency() public view returns (bool) {
return address(this).balance + registry.totalLocked() == INITIAL_BALANCE;
}
3. State Transition Properties¶
Test valid state transitions:
function property_status_progression() public view returns (bool) {
// Once executed, status cannot revert
if (proposal.status == ProposalStatus.Executed) {
return proposal.status == ProposalStatus.Executed;
}
return true;
}
Running Medusa¶
Basic Fuzzing¶
With Timeout¶
Specific Test¶
With Coverage¶
CI/CD Integration¶
Medusa runs automatically in the GitHub Actions workflow:
Job: medusa-fuzzing
- name: Install Medusa
run: |
go install github.com/crytic/medusa@latest
echo "$HOME/go/bin" >> $GITHUB_PATH
- name: Run Medusa fuzzing
run: |
medusa fuzz --timeout 300 || true
Output and Results¶
Console Output¶
[Medusa] Starting fuzzing campaign
[Medusa] Workers: 10, Timeout: 300s
[Medusa] Target contracts: ProposalRegistryFuzzTest, WelfareMetricRegistryFuzzTest
[Worker 1] Fuzzing ProposalRegistryFuzzTest
[Worker 1] Corpus size: 42 | Coverage: 87% | Executions: 1,524
✓ property_proposal_count_never_decreases: PASSED
✓ property_bond_amount_positive: PASSED
✓ property_total_weight_bounded: PASSED
[Medusa] Fuzzing completed
[Medusa] Total tests: 3 | Passed: 3 | Failed: 0
[Medusa] Coverage: 87% | Time: 178s
Failure Output¶
When a property fails:
✗ property_total_supply_bounded: FAILED
Failing sequence:
1. constructor()
2. mint(0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2, 1000000000)
3. mint(0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2, MAX_UINT256)
Property returned false at:
File: ProposalRegistryFuzzTest.sol
Function: property_total_supply_bounded
Transaction trace saved to: medusa-corpus/failed_001.json
Corpus Directory¶
Medusa saves interesting test cases:
medusa-corpus/
├── coverage/ # High-coverage sequences
├── failed/ # Property-violating sequences
└── optimization/ # Gas optimization opportunities
Interpreting Results¶
Passed Properties¶
All tested property functions returned true:
- Invariants hold under random inputs
- No edge cases found that violate properties
- Contract behaves correctly under fuzzing
Failed Properties¶
A property function returned false:
- Review the failing transaction sequence
- Understand why the property was violated
- Fix the contract or adjust the property
- Re-run fuzzing to verify the fix
Coverage Metrics¶
Higher coverage means more thorough testing.
Fuzz Test Examples¶
ProposalRegistry¶
contract ProposalRegistryFuzzTest {
ProposalRegistry public registry;
constructor() {
registry = new ProposalRegistry();
}
// Invariant: Count never decreases
function property_proposal_count_never_decreases() public view returns (bool) {
uint256 count = registry.proposalCount();
return count >= 0;
}
// Invariant: Bond amount is positive
function property_bond_amount_positive() public view returns (bool) {
return registry.bondAmount() > 0;
}
// Property: Submission requires correct bond
function property_submission_requires_bond(
string memory title,
uint256 fundingAmount
) public payable returns (bool) {
// Test various conditions
if (msg.value < registry.bondAmount()) {
return true; // Should revert
}
if (bytes(title).length == 0) {
return true; // Should revert
}
return true;
}
}
WelfareMetricRegistry¶
contract WelfareMetricRegistryFuzzTest {
WelfareMetricRegistry public registry;
constructor() {
registry = new WelfareMetricRegistry();
}
// Invariant: Total weight bounded
function property_total_weight_bounded() public view returns (bool) {
return registry.totalActiveWeight() <= 10000;
}
// Invariant: Metric count never decreases
function property_metric_count_never_decreases() public view returns (bool) {
return registry.metricCount() >= 0;
}
// Property: Weight values are valid
function property_weight_bounded(uint256 weight) public pure returns (bool) {
return weight <= 10000;
}
}
Best Practices¶
When writing fuzz tests:
- Test critical invariants: Focus on properties that must never be violated
- Keep properties simple: Complex logic makes debugging harder
- Use meaningful names: Clearly describe what each property tests
- Return early for invalid inputs: Handle edge cases gracefully
- Cover multiple scenarios: Test different contract states
- Document assumptions: Explain why properties should hold
- Run regularly: Integrate into CI/CD for continuous testing
Advanced Usage¶
Custom Assertions¶
function property_complex_invariant() public returns (bool) {
uint256 before = registry.totalSupply();
// Perform operations
registry.mint(address(this), 100);
uint256 after = registry.totalSupply();
// Check invariant maintained
return after == before + 100;
}
State Exploration¶
function property_state_transition() public returns (bool) {
ProposalStatus status = registry.getStatus(0);
// Try to advance state
try registry.advanceProposal(0) {
// Verify valid transition
ProposalStatus newStatus = registry.getStatus(0);
return isValidTransition(status, newStatus);
} catch {
// Revert is acceptable
return true;
}
}
Optimization Testing¶
function optimize_batch_operation() public returns (bool) {
// Test gas efficiency
uint256 gasBefore = gasleft();
registry.batchOperation([1, 2, 3, 4, 5]);
uint256 gasUsed = gasBefore - gasleft();
// Should use less than individual operations
return gasUsed < EXPECTED_GAS_LIMIT;
}
Troubleshooting¶
No Properties Found¶
Check:
- Functions start with property_ prefix
- Functions are public or external
- Functions return bool
- Target contracts are listed in config
Slow Fuzzing¶
Solutions:
- Reduce callSequenceLength
- Decrease number of workers
- Use shorter timeout
- Simplify property functions
High Memory Usage¶
Solutions:
- Reduce workerResetLimit
- Decrease workers
- Clear corpus directory
- Simplify test contracts
False Positives¶
Solutions: - Review property logic - Add input validation - Handle edge cases in properties - Document expected behavior
Comparison with Unit Tests¶
| Aspect | Fuzz Testing | Unit Testing |
|---|---|---|
| Input | Random | Predetermined |
| Coverage | Broader | Targeted |
| Edge Cases | Discovers | Must specify |
| Speed | Slower | Faster |
| Determinism | Non-deterministic | Deterministic |
| Debugging | Harder | Easier |
Use both: - Unit tests for known scenarios - Fuzz tests for unknown edge cases