Medusa

A parallelized smart contract fuzzer based on Echidna's approach, offering faster execution through concurrent testing across multiple workers.

Medusa is a smart contract fuzzer developed by Trail of Bits that builds on Echidna's proven approach to property-based testing while adding parallelization for significantly faster execution. Written in Go, Medusa leverages multiple CPU cores to run concurrent fuzzing workers, making it particularly effective for large test suites or time-constrained security testing. It maintains compatibility with Echidna's property conventions while offering additional features and improved performance.

Medusa vs Echidna

Medusa was created to address Echidna's single-threaded limitation:

FeatureEchidnaMedusa
LanguageHaskellGo
ParallelizationSingle-threadedMulti-worker
SpeedBaseline2-10x faster
Property syntaxechidna_*echidna_* (compatible)
Corpus sharingPer-runShared across workers
ShrinkingAdvancedGood
MaturityEstablishedNewer

For most use cases, Medusa offers the same testing methodology with better performance. Echidna remains valuable for its more sophisticated shrinking and edge case handling.

Installation and Usage

1# Install via Go
2go install github.com/crytic/medusa@latest
3
4# Or download binary from releases
5# https://github.com/crytic/medusa/releases
6
7# Initialize config in your project
8medusa init
9
10# Run fuzzing
11medusa fuzz

Medusa generates a medusa.json config file with sensible defaults.

Configuration

Basic medusa.json:

1{
2 "fuzzing": {
3 "workers": 4,
4 "testLimit": 100000,
5 "callSequenceLength": 100,
6 "corpusDirectory": "corpus",
7 "coverageEnabled": true
8 },
9 "compilation": {
10 "platform": "crytic-compile",
11 "platformConfig": {
12 "target": ".",
13 "solcVersion": "0.8.19"
14 }
15 },
16 "testing": {
17 "propertyTesting": {
18 "enabled": true
19 },
20 "assertionTesting": {
21 "enabled": true
22 }
23 }
24}

Key settings:

  • workers: Number of parallel fuzzing workers (set to CPU cores)
  • testLimit: Total transactions to execute across all workers
  • callSequenceLength: Maximum transaction sequence length
  • corpusDirectory: Where to save interesting inputs

Writing Medusa Tests

Medusa uses the same conventions as Echidna:

1contract MedusaTest {
2 Token token;
3
4 constructor() {
5 token = new Token();
6 token.mint(address(this), 1000000);
7 }
8
9 // Property: must always return true
10 function echidna_supply_matches_minted() public view returns (bool) {
11 return token.totalSupply() == 1000000;
12 }
13
14 // Functions Medusa will call with random inputs
15 function transfer(address to, uint256 amount) public {
16 token.transfer(to, amount);
17 }
18
19 function burn(uint256 amount) public {
20 token.burn(amount);
21 }
22}

The echidna_ prefix works with Medusa, making migration between tools seamless.

Parallel Fuzzing Architecture

Medusa's parallelization works through:

  1. Multiple workers: Each worker executes independent transaction sequences
  2. Shared corpus: Interesting inputs discovered by one worker benefit all workers
  3. Coordinated coverage: Workers collectively maximize code coverage
  4. Lock-free design: Minimal synchronization overhead
1┌─────────────────────────────────────────┐
2│ Medusa Controller │
3├─────────────────────────────────────────┤
4│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
5│ │ Worker 1│ │ Worker 2│ │ Worker 3│ │
6│ │ EVM │ │ EVM │ │ EVM │ │
7│ └────┬────┘ └────┬────┘ └────┬────┘ │
8│ │ │ │ │
9│ └───────────┴───────────┘ │
10│ Shared Corpus │
11└─────────────────────────────────────────┘

Test Modes

Property Testing

The default mode—tests that invariants hold:

1function echidna_invariant() public view returns (bool) {
2 return someCondition;
3}

Assertion Testing

Tests that assertions don't fail:

1function test_operation(uint256 x) public {
2 uint256 result = contract.operation(x);
3 assert(result > 0); // Medusa checks this doesn't fail
4}

Optimization Testing

Finds inputs that maximize/minimize values:

1function optimize_maximize_value() public view returns (int256) {
2 return int256(contract.someValue());
3}

Coverage-Guided Fuzzing

Medusa prioritizes inputs that explore new code paths:

1{
2 "fuzzing": {
3 "coverageEnabled": true,
4 "corpusDirectory": "corpus"
5 }
6}

Coverage data guides mutation:

  1. Execute transaction sequence
  2. Record which code paths were hit
  3. If new paths discovered, save inputs to corpus
  4. Mutate corpus entries to find more new paths

Real-World Example: Testing a Vault

1contract VaultTest {
2 Vault vault;
3 ERC20 token;
4
5 address[] users = [address(0x1), address(0x2), address(0x3)];
6
7 constructor() {
8 token = new ERC20("Test", "TST");
9 vault = new Vault(address(token));
10
11 // Setup initial state
12 for (uint i = 0; i < users.length; i++) {
13 token.mint(users[i], 10000 ether);
14 }
15 }
16
17 // Actions
18 function deposit(uint8 userIdx, uint256 amount) public {
19 address user = users[userIdx % users.length];
20 vm.prank(user);
21 token.approve(address(vault), amount);
22 vm.prank(user);
23 vault.deposit(amount);
24 }
25
26 function withdraw(uint8 userIdx, uint256 shares) public {
27 address user = users[userIdx % users.length];
28 vm.prank(user);
29 vault.withdraw(shares);
30 }
31
32 // Invariants
33 function echidna_vault_solvent() public view returns (bool) {
34 return token.balanceOf(address(vault)) >= vault.totalAssets();
35 }
36
37 function echidna_shares_backed() public view returns (bool) {
38 if (vault.totalSupply() == 0) return true;
39 return vault.totalAssets() > 0;
40 }
41}

Run with multiple workers:

1medusa fuzz --workers 8

Integration with Foundry

Medusa works alongside Foundry in the same project:

1project/
2├── src/
3│ └── Vault.sol
4├── test/
5│ ├── Vault.t.sol # Foundry tests
6│ └── VaultMedusa.sol # Medusa properties
7├── foundry.toml
8└── medusa.json

Use Foundry for unit tests and quick fuzzing, Medusa for deep invariant testing with parallelization.

CI/CD Integration

1# GitHub Actions
2- name: Run Medusa
3 run: |
4 medusa fuzz --workers 4 --test-limit 100000

For large projects, run Medusa in CI with higher test limits during nightly builds.

Best Practices

  1. Maximize workers: Set workers equal to available CPU cores
  2. Persist corpus: Keep the corpus directory in version control or CI cache
  3. Combine with Echidna: Use both—Echidna's shrinking can provide better counterexamples
  4. Start with short sequences: Begin with callSequenceLength: 50, increase if needed
  5. Bound inputs realistically: Use require() to constrain inputs to valid ranges

Limitations

Memory usage: Multiple workers multiply memory consumption.

Counterexample quality: Shrinking is less sophisticated than Echidna's.

Debugging: Parallel execution can make reproducing issues trickier.

Medusa represents the evolution of smart contract fuzzing—same battle-tested methodology as Echidna, but leveraging modern hardware through parallelization. For security-critical code, running both Echidna and Medusa provides complementary coverage and counterexample quality.

Need expert guidance on Medusa?

Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.

Get a Quote

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx