This file provides guidance to LLM Agents (e.g. Claude Code or Codex) when working with code in this repository.
Medusa is a cross-platform smart contract fuzzer written in Go, built on go-ethereum and inspired by Echidna. It provides parallelized, coverage-guided fuzz testing of Solidity smart contracts through CLI or Go API. The fuzzer supports assertion testing, property testing, optimization testing, and cheat codes.
go build- Compile the CLI binary for local testingnix build- Build using Nix (run after modifyinggo.modto refresh vendorHash)./medusa --help- Verify the built binary
go test -v ./...- Run all unit and integration testsgo test -v ./fuzzing/...- Run tests for a specific package with verbose outputgo test -v -run TestName ./package/...- Run a specific unit test by namego test -cover ./...- Run tests with coverage metrics
goimports -w .- Format imports and code (groups stdlib, external, local)go fmt ./...- Format Go code (required before commits)golangci-lint run --timeout 5m- Run comprehensive lint checks (mirrors CI)dprint fmt- Format markdown, YAML, and JSON filesactionlint- Lint GitHub Actions workflow files
go run . --config path/to/config.yaml- Compile and run the fuzzernix develop- Enter the pinned Nix development shell with all dependencies
prek install- Install pre-commit hooks (run once after cloning)prek run- Manually run all pre-commit checks (use withinnix developshell)
cmd/- CLI entry point using Cobra framework. Definesfuzz,init, andcompletioncommands.fuzzing/- Core fuzzing orchestration including workers, corpus management, coverage tracking, value generation, and test case providers.chain/- EVM test harness (TestChain) built on medusa-geth with state management and cheat code support (vm.* functions).compilation/- Smart contract compilation abstraction with platform adapters for solc and crytic-compile.logging/- Structured logging primitives using Zerolog.events/- Event emitter system for fuzzer lifecycle hooks and extensibility.utils/- Shared utilities for random values, reflection, file operations.
CLI (main.go)
↓
cmd/fuzz.go (Cobra Command)
↓
fuzzing.NewFuzzer(ProjectConfig)
├─→ Compilation (compile contracts via crytic-compile/solc)
├─→ FuzzerWorker pool (N parallel workers)
├─→ TestChain per worker (isolated EVM instance)
├─→ Corpus (loads/saves coverage-increasing call sequences)
├─→ Test Case Providers (Assertion, Property, Optimization)
└─→ Coverage Tracer (if enabled)
Worker Loop (per worker, in parallel):
1. Deploy contracts via ChainSetupFunc hook
2. Generate CallSequence (mutations from corpus)
3. Execute sequence on TestChain
4. Run CallSequenceTestFuncs (test case providers)
5. If test fails → shrink sequence → save to corpus
6. Update coverage maps
7. Reset chain state to base block → repeat
Event-Driven Extensibility: FuzzerEvents emits lifecycle events (FuzzerStarting, FuzzerStopping, WorkerCreated, WorkerDestroyed). Test case providers subscribe to these events and register CallSequenceTestFunc hooks, enabling new test types without modifying core logic.
Hook-Based Customization: FuzzerHooks provides customization points:
NewCallSequenceGeneratorConfigFunc- Customize sequence generation strategyNewShrinkingValueMutatorFunc- Customize shrinking heuristicsChainSetupFunc- Customize deployment and initialization logicCallSequenceTestFuncs[]- Register test functions to run after each sequence
Coverage-Guided Fuzzing: The corpus stores only call sequences that increase coverage. Sequences are mutated using weighted strategies (corpus head/tail, splice, interleave, new). A corpus pruner periodically removes redundant sequences to keep the corpus lean.
Worker-Based Parallelization: Each FuzzerWorker has its own isolated TestChain instance with no shared state. Workers are periodically destroyed and recreated (WorkerResetLimit) to prevent memory bloat from geth's state accumulation.
Call Sequence Shrinking: When a test fails, a ShrinkCallSequenceRequest is generated. The worker iteratively removes/mutates calls while a VerifierFunction confirms the test still fails, producing a minimal reproduction.
State Management: TestChain rebases to testingBaseBlockIndex after each sequence execution, providing clean state for the next sequence. MedusaStateDB interface abstracts underlying state implementation (supports native geth StateDB and fork mode).
CallSequence- Array ofCallSequenceElementrepresenting a transaction sequence to execute on the test chain.CallSequenceElement- Single call with target address, method, arguments, block delay, and gas limit.TestCaseinterface - Represents a test with Status (NOT_STARTED, RUNNING, PASSED, FAILED), Name, CallSequence, and result Message.Corpus- Persistent storage of coverage-increasing call sequences with pruning. Stored on disk incorpus/directory.CoverageMaps- Tracks branch coverage per contract (including jumps, returns, reverts, and contract entrance), used to identify coverage-increasing sequences.FuzzerWorker- Single execution thread with its own TestChain, deployed contracts, and sequence generator.ProjectConfig- Top-level configuration containing FuzzingConfig, CompilationConfig, LoggingConfig.FuzzingConfig- Workers count, timeout, test limit, corpus directory, coverage settings, test case configurations.
Three built-in test case providers run concurrently:
- AssertionTestCaseProvider: Monitors EVM-level panic conditions for each contract method (e.g.,
assert(), arithmetic underflow/overflow, divide by zero, array access violations). Which panic codes trigger test failures is configured viaPanicCodeConfiginFuzzingConfig.Testing.AssertionTesting. - PropertyTestCaseProvider: Calls property test functions (prefix-based naming, must be view functions returning bool). If a property returns false, the test fails.
- OptimizationTestCaseProvider: Tracks optimization targets (e.g., maximize gas usage) and shrinks sequences to find minimal paths to targets.
- Co-locate tests beside implementations using
*_test.gosuffix - Use table-driven tests for deterministic logic
- Add
t.Parallel()for parallelizable tests - Document invariants with assertions
go test -v ./...- Run all testsgo test -v ./fuzzing/...- Target specific packagesgo test -v -run TestName ./package/...- Run a specific unit test
Explicitly ask the user if these scripts should be run. In more cases than not, these scripts don't need to be touched.
- Use
python3 scripts/corpus_diff.py old newto compare corpora and identify method coverage differences - Use
python3 scripts/corpus_stats.py corpusto generate statistics (sequence count, average length, method frequency)
- Branch naming:
dev/<short-scope>(e.g.,dev/coverage-reports) - Commit format:
area: intent(e.g.,fuzzing: tighten revert handling) - Reference issues with
(#123)when applicable
- Naming: Packages lowercase (
fuzzing,chain), exported types UpperCamelCase (TestChain), private helpers lowerCamel (newWorker) - Documentation: All exported symbols need doc comments. Add inline comments for complex logic.
- File naming: Max 32 characters to avoid Windows path length issues
- JSON keys: Use camelCase rather than snake_case
- Code must work on Linux, macOS, and Windows
- Use
filepathpackage (notpath) for file operations - respects system path separators - Support both LF (
\n) and CRLF (\r\n) line endings when processing text files - Test on multiple platforms before submitting PRs
- When dependencies change, update
vendorHashinflake.nix - Run
nix build- if vendorHash needs updating, Nix will report the correct hash to use - Replace the
specifiedvalue with thegotvalue from the error message
The following checks run automatically via pre-commit hooks (install with prek install):
goimports- Format imports (hook verifies no changes)go fmt ./...- Format Go code (hook verifies no changes)golangci-lint run --timeout 5m- Run linterdprint check- Verify markdown/YAML/JSON formattingactionlint- Lint GitHub Actions workflows
Manual checks before submitting PRs:
go test -v ./...- Run all tests (too slow for pre-commit)- Verify changes work on target platforms