Skip to content

Commit 2ed4855

Browse files
authored
Merge branch 'master' into feat/rework-mutators
2 parents 8b02374 + 3a9b0fa commit 2ed4855

File tree

9 files changed

+173
-70
lines changed

9 files changed

+173
-70
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
`medusa` is a cross-platform [go-ethereum](https://github.com/ethereum/go-ethereum/)-based smart contract fuzzer inspired by [Echidna](https://github.com/crytic/echidna).
44
It provides parallelized fuzz testing of smart contracts through CLI, or its Go API that allows custom user-extended testing methodology.
55

6-
**Disclaimer**: Please note that `medusa` is an **experimental** smart contract fuzzer. Currently, it should _not_ be adopted into production systems. We intend for `medusa` to reach the same capabilities and maturity that Echidna has. Until then, be careful using `medusa` as your primary smart contract fuzz testing solution. Additionally, please be aware that the Go-level testing API is still **under development** and is subject to breaking changes.
6+
**Disclaimer**: The Go-level testing API is still **under development** and is subject to breaking changes.
77

88
## Features
99

@@ -29,6 +29,23 @@ cd docs
2929
mdbook serve
3030
```
3131

32+
## Install
33+
34+
MacOS users can install the latest release of `medusa` using Homebrew:
35+
36+
```shell
37+
38+
brew install medusa
39+
```
40+
41+
The master branch can be installed using the following command:
42+
43+
```shell
44+
brew install --HEAD medusa
45+
```
46+
47+
For more information on building from source or obtaining binaries for Windows and Linux, please refer to the [installation guide](./docs/src/getting_started/installation.md).
48+
3249
## Contributing
3350

3451
For information about how to contribute to this project, check out the [CONTRIBUTING](./CONTRIBUTING.md) guidelines.

fuzzing/corpus/corpus.go

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import (
44
"bytes"
55
"fmt"
66
"math/big"
7+
"os"
78
"path/filepath"
89
"sync"
910
"time"
1011

12+
"github.com/crytic/medusa/utils"
13+
1114
"github.com/crytic/medusa/chain"
1215
"github.com/crytic/medusa/fuzzing/calls"
1316
"github.com/crytic/medusa/fuzzing/coverage"
@@ -30,13 +33,8 @@ type Corpus struct {
3033
// coverageMaps describes the total code coverage known to be achieved across all corpus call sequences.
3134
coverageMaps *coverage.CoverageMaps
3235

33-
// mutableSequenceFiles represents a corpus directory with files which describe call sequences that should
34-
// be used for mutations.
35-
mutableSequenceFiles *corpusDirectory[calls.CallSequence]
36-
37-
// immutableSequenceFiles represents a corpus directory with files which describe call sequences that should not be
38-
// used for mutations.
39-
immutableSequenceFiles *corpusDirectory[calls.CallSequence]
36+
// callSequenceFiles represents a corpus directory with files that should be used for mutations.
37+
callSequenceFiles *corpusDirectory[calls.CallSequence]
4038

4139
// testResultSequenceFiles represents a corpus directory with files which describe call sequences that were flagged
4240
// to be saved by a test case provider. These are not used in mutations.
@@ -66,25 +64,25 @@ func NewCorpus(corpusDirectory string) (*Corpus, error) {
6664
corpus := &Corpus{
6765
storageDirectory: corpusDirectory,
6866
coverageMaps: coverage.NewCoverageMaps(),
69-
mutableSequenceFiles: newCorpusDirectory[calls.CallSequence](""),
70-
immutableSequenceFiles: newCorpusDirectory[calls.CallSequence](""),
67+
callSequenceFiles: newCorpusDirectory[calls.CallSequence](""),
7168
testResultSequenceFiles: newCorpusDirectory[calls.CallSequence](""),
7269
unexecutedCallSequences: make([]calls.CallSequence, 0),
7370
logger: logging.GlobalLogger.NewSubLogger("module", "corpus"),
7471
}
7572

7673
// If we have a corpus directory set, parse our call sequences.
7774
if corpus.storageDirectory != "" {
78-
// Read mutable call sequences.
79-
corpus.mutableSequenceFiles.path = filepath.Join(corpus.storageDirectory, "call_sequences", "mutable")
80-
err = corpus.mutableSequenceFiles.readFiles("*.json")
75+
// Migrate the legacy corpus structure
76+
// Note that it is important to call this first since we want to move all the call sequence files before reading
77+
// them into the corpus
78+
err = corpus.migrateLegacyCorpus()
8179
if err != nil {
8280
return nil, err
8381
}
8482

85-
// Read immutable call sequences.
86-
corpus.immutableSequenceFiles.path = filepath.Join(corpus.storageDirectory, "call_sequences", "immutable")
87-
err = corpus.immutableSequenceFiles.readFiles("*.json")
83+
// Read call sequences.
84+
corpus.callSequenceFiles.path = filepath.Join(corpus.storageDirectory, "call_sequences")
85+
err = corpus.callSequenceFiles.readFiles("*.json")
8886
if err != nil {
8987
return nil, err
9088
}
@@ -100,26 +98,90 @@ func NewCorpus(corpusDirectory string) (*Corpus, error) {
10098
return corpus, nil
10199
}
102100

101+
// migrateLegacyCorpus is used to read in the legacy corpus standard where call sequences were stored in two separate
102+
// directories (mutable/immutable).
103+
func (c *Corpus) migrateLegacyCorpus() error {
104+
// Check to see if the mutable and/or the immutable directories exist
105+
callSequencePath := filepath.Join(c.storageDirectory, "call_sequences")
106+
mutablePath := filepath.Join(c.storageDirectory, "call_sequences", "mutable")
107+
immutablePath := filepath.Join(c.storageDirectory, "call_sequences", "immutable")
108+
109+
// Only return an error if the error is something other than "filepath does not exist"
110+
mutableDirInfo, err := os.Stat(mutablePath)
111+
if err != nil && !os.IsNotExist(err) {
112+
return err
113+
}
114+
immutableDirInfo, err := os.Stat(immutablePath)
115+
if err != nil && !os.IsNotExist(err) {
116+
return err
117+
}
118+
119+
// Return early if these directories do not exist
120+
if mutableDirInfo == nil && immutableDirInfo == nil {
121+
return nil
122+
}
123+
124+
// Now, we need to notify the user that we have detected a legacy structure
125+
c.logger.Info("Migrating legacy corpus")
126+
127+
// If the mutable directory exists, read in all the files and add them to the call sequence files
128+
if mutableDirInfo != nil {
129+
// Discover all corpus files in the given directory.
130+
filePaths, err := filepath.Glob(filepath.Join(mutablePath, "*.json"))
131+
if err != nil {
132+
return err
133+
}
134+
135+
// Move each file from the mutable directory to the parent call_sequences directory
136+
for _, filePath := range filePaths {
137+
err = utils.MoveFile(filePath, filepath.Join(callSequencePath, filepath.Base(filePath)))
138+
if err != nil {
139+
return err
140+
}
141+
}
142+
143+
// Delete the mutable directory
144+
err = utils.DeleteDirectory(mutablePath)
145+
if err != nil {
146+
return err
147+
}
148+
}
149+
150+
// If the immutable directory exists, read in all the files and add them to the call sequence files
151+
if immutableDirInfo != nil {
152+
// Discover all corpus files in the given directory.
153+
filePaths, err := filepath.Glob(filepath.Join(immutablePath, "*.json"))
154+
if err != nil {
155+
return err
156+
}
157+
158+
// Move each file from the immutable directory to the parent call_sequences directory
159+
for _, filePath := range filePaths {
160+
err = utils.MoveFile(filePath, filepath.Join(callSequencePath, filepath.Base(filePath)))
161+
if err != nil {
162+
return err
163+
}
164+
}
165+
166+
// Delete the immutable directory
167+
err = utils.DeleteDirectory(immutablePath)
168+
if err != nil {
169+
return err
170+
}
171+
}
172+
173+
return nil
174+
}
175+
103176
// CoverageMaps exposes coverage details for all call sequences known to the corpus.
104177
func (c *Corpus) CoverageMaps() *coverage.CoverageMaps {
105178
return c.coverageMaps
106179
}
107180

108-
// CallSequenceEntryCount returns the total number of call sequences entries in the corpus, based on the provided filter
109-
// flags. Some call sequences may not be valid for use if they fail validation when initializing the corpus.
110-
// Returns the count of the requested call sequence entries.
111-
func (c *Corpus) CallSequenceEntryCount(mutable bool, immutable bool, testResults bool) int {
112-
count := 0
113-
if mutable {
114-
count += len(c.mutableSequenceFiles.files)
115-
}
116-
if immutable {
117-
count += len(c.immutableSequenceFiles.files)
118-
}
119-
if testResults {
120-
count += len(c.testResultSequenceFiles.files)
121-
}
122-
return count
181+
// CallSequenceEntryCount returns the total number of call sequences that increased coverage and also any test results
182+
// that led to a failure.
183+
func (c *Corpus) CallSequenceEntryCount() (int, int) {
184+
return len(c.callSequenceFiles.files), len(c.testResultSequenceFiles.files)
123185
}
124186

125187
// ActiveMutableSequenceCount returns the count of call sequences recorded in the corpus which have been validated
@@ -302,18 +364,13 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions
302364
return 0, 0, err
303365
}
304366

305-
err = c.initializeSequences(c.mutableSequenceFiles, testChain, deployedContracts, true)
306-
if err != nil {
307-
return 0, 0, err
308-
}
309-
310-
err = c.initializeSequences(c.immutableSequenceFiles, testChain, deployedContracts, false)
367+
err = c.initializeSequences(c.callSequenceFiles, testChain, deployedContracts, true)
311368
if err != nil {
312369
return 0, 0, err
313370
}
314371

315372
// Calculate corpus health metrics
316-
corpusSequencesTotal := len(c.mutableSequenceFiles.files) + len(c.immutableSequenceFiles.files) + len(c.testResultSequenceFiles.files)
373+
corpusSequencesTotal := len(c.callSequenceFiles.files) + len(c.testResultSequenceFiles.files)
317374
corpusSequencesActive := len(c.unexecutedCallSequences)
318375

319376
return corpusSequencesActive, corpusSequencesTotal, nil
@@ -411,17 +468,9 @@ func (c *Corpus) CheckSequenceCoverageAndUpdate(callSequence calls.CallSequence,
411468
}
412469

413470
// If we had an increase in non-reverted or reverted coverage, we save the sequence.
414-
// Note: We only want to save the sequence once. We're most interested if it can be used for mutations first.
415-
if coverageUpdated {
416-
// If we achieved new non-reverting coverage, save this sequence for mutation purposes.
417-
err = c.addCallSequence(c.mutableSequenceFiles, callSequence, true, mutationChooserWeight, flushImmediately)
418-
if err != nil {
419-
return err
420-
}
421-
} else if revertedCoverageUpdated {
422-
// If we did not achieve new successful coverage, but achieved an increase in reverted coverage, save this
423-
// sequence for non-mutation purposes.
424-
err = c.addCallSequence(c.immutableSequenceFiles, callSequence, false, mutationChooserWeight, flushImmediately)
471+
if coverageUpdated || revertedCoverageUpdated {
472+
// If we achieved new coverage, save this sequence for mutation purposes.
473+
err = c.addCallSequence(c.callSequenceFiles, callSequence, true, mutationChooserWeight, flushImmediately)
425474
if err != nil {
426475
return err
427476
}
@@ -470,8 +519,8 @@ func (c *Corpus) Flush() error {
470519
c.callSequencesLock.Lock()
471520
defer c.callSequencesLock.Unlock()
472521

473-
// Write mutation target call sequences.
474-
err := c.mutableSequenceFiles.writeFiles()
522+
// Write all coverage-increasing call sequences.
523+
err := c.callSequenceFiles.writeFiles()
475524
if err != nil {
476525
return err
477526
}
@@ -482,11 +531,5 @@ func (c *Corpus) Flush() error {
482531
return err
483532
}
484533

485-
// Write other call sequences.
486-
err = c.immutableSequenceFiles.writeFiles()
487-
if err != nil {
488-
return err
489-
}
490-
491534
return nil
492535
}

fuzzing/corpus/corpus_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func getMockSimpleCorpus(minSequences int, maxSequences, minBlocks int, maxBlock
2323
// Add the requested number of entries.
2424
numSequences := minSequences + (rand.Int() % (maxSequences - minSequences))
2525
for i := 0; i < numSequences; i++ {
26-
err := corpus.addCallSequence(corpus.mutableSequenceFiles, getMockCallSequence(minBlocks+(rand.Int()%(maxBlocks-minBlocks))), true, nil, false)
26+
err := corpus.addCallSequence(corpus.callSequenceFiles, getMockCallSequence(minBlocks+(rand.Int()%(maxBlocks-minBlocks))), true, nil, false)
2727
if err != nil {
2828
return nil, err
2929
}
@@ -100,9 +100,9 @@ func TestCorpusReadWrite(t *testing.T) {
100100
assert.NoError(t, err)
101101

102102
// Ensure that there are the correct number of call sequence files
103-
matches, err := filepath.Glob(filepath.Join(corpus.mutableSequenceFiles.path, "*.json"))
103+
matches, err := filepath.Glob(filepath.Join(corpus.callSequenceFiles.path, "*.json"))
104104
assert.NoError(t, err)
105-
assert.EqualValues(t, len(corpus.mutableSequenceFiles.files), len(matches))
105+
assert.EqualValues(t, len(corpus.callSequenceFiles.files), len(matches))
106106

107107
// Wipe corpus clean so that you can now read it in from disk
108108
corpus, err = NewCorpus("corpus")
@@ -124,7 +124,7 @@ func TestCorpusCallSequenceMarshaling(t *testing.T) {
124124
// Run the test in our temporary test directory to avoid artifact pollution.
125125
testutils.ExecuteInDirectory(t, t.TempDir(), func() {
126126
// For each entry, marshal it and then unmarshal the byte array
127-
for _, entryFile := range corpus.mutableSequenceFiles.files {
127+
for _, entryFile := range corpus.callSequenceFiles.files {
128128
// Marshal the entry
129129
b, err := json.Marshal(entryFile.data)
130130
assert.NoError(t, err)
@@ -139,9 +139,9 @@ func TestCorpusCallSequenceMarshaling(t *testing.T) {
139139
}
140140

141141
// Remove all items
142-
for i := 0; i < len(corpus.mutableSequenceFiles.files); {
143-
corpus.mutableSequenceFiles.removeFile(corpus.mutableSequenceFiles.files[i].fileName)
142+
for i := 0; i < len(corpus.callSequenceFiles.files); {
143+
corpus.callSequenceFiles.removeFile(corpus.callSequenceFiles.files[i].fileName)
144144
}
145-
assert.Empty(t, corpus.mutableSequenceFiles.files)
145+
assert.Empty(t, corpus.callSequenceFiles.files)
146146
})
147147
}

fuzzing/executiontracer/execution_tracer.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ func (t *ExecutionTracer) GetTrace(txHash common.Hash) *ExecutionTrace {
110110

111111
// OnTxEnd is called upon the end of transaction execution, as defined by tracers.Tracer.
112112
func (t *ExecutionTracer) OnTxEnd(receipt *coretypes.Receipt, err error) {
113+
// We avoid storing the trace for this transaction. An error should realistically only occur if we hit a block gas
114+
// limit error. In this case, the transaction will be retried in the next block and we can retrieve the trace at
115+
// that time.
116+
if err != nil || receipt == nil {
117+
return
118+
}
113119
t.traceMap[receipt.TxHash] = t.trace
114120
}
115121

fuzzing/fuzzer.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,14 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex
419419
// Ordering is important here (predeploys _then_ targets) so that you can have the same contract in both lists
420420
// while still being able to use the contract address overrides
421421
contractsToDeploy := make([]string, 0)
422+
balances := make([]*big.Int, 0)
422423
for contractName := range fuzzer.config.Fuzzing.PredeployedContracts {
423424
contractsToDeploy = append(contractsToDeploy, contractName)
425+
// Preserve index of target contract balances
426+
balances = append(balances, big.NewInt(0))
424427
}
425428
contractsToDeploy = append(contractsToDeploy, fuzzer.config.Fuzzing.TargetContracts...)
429+
balances = append(balances, fuzzer.config.Fuzzing.TargetContractsBalances...)
426430

427431
deployedContractAddr := make(map[string]common.Address)
428432
// Loop for all contracts to deploy
@@ -460,8 +464,8 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex
460464

461465
// If our project config has a non-zero balance for this target contract, retrieve it
462466
contractBalance := big.NewInt(0)
463-
if len(fuzzer.config.Fuzzing.TargetContractsBalances) > i {
464-
contractBalance = new(big.Int).Set(fuzzer.config.Fuzzing.TargetContractsBalances[i])
467+
if len(balances) > i {
468+
contractBalance = new(big.Int).Set(balances[i])
465469
}
466470

467471
// Create a message to represent our contract deployment (we let deployments consume the whole block
@@ -758,8 +762,8 @@ func (f *Fuzzer) Start() error {
758762

759763
// Initialize our coverage maps by measuring the coverage we get from the corpus.
760764
var corpusActiveSequences, corpusTotalSequences int
761-
if f.corpus.CallSequenceEntryCount(true, true, true) > 0 {
762-
f.logger.Info("Running call sequences in the corpus...")
765+
if totalCallSequences, testResults := f.corpus.CallSequenceEntryCount(); totalCallSequences > 0 || testResults > 0 {
766+
f.logger.Info("Running call sequences in the corpus")
763767
}
764768
startTime := time.Now()
765769
corpusActiveSequences, corpusTotalSequences, err = f.corpus.Initialize(baseTestChain, f.contractDefinitions)

fuzzing/fuzzer_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ func TestDeploymentsWithPredeploy(t *testing.T) {
403403
filePath: "testdata/contracts/deployments/predeploy_contract.sol",
404404
configUpdates: func(config *config.ProjectConfig) {
405405
config.Fuzzing.TargetContracts = []string{"TestContract"}
406+
config.Fuzzing.TargetContractsBalances = []*big.Int{big.NewInt(1)}
406407
config.Fuzzing.TestLimit = 1000 // this test should expose a failure immediately
407408
config.Fuzzing.Testing.PropertyTesting.Enabled = false
408409
config.Fuzzing.Testing.OptimizationTesting.Enabled = false
@@ -825,7 +826,8 @@ func TestCorpusReplayability(t *testing.T) {
825826

826827
// Cache current coverage maps
827828
originalCoverage := f.fuzzer.corpus.CoverageMaps()
828-
originalCorpusSequenceCount := f.fuzzer.corpus.CallSequenceEntryCount(true, true, true)
829+
originalTotalCallSequences, originalTotalTestResults := f.fuzzer.corpus.CallSequenceEntryCount()
830+
originalCorpusSequenceCount := originalTotalCallSequences + originalTotalTestResults
829831

830832
// Next, set the fuzzer worker count to one, this allows us to count the call sequences executed before
831833
// solving a problem. We will verify the problem is solved with less or equal sequences tested, than

fuzzing/fuzzer_test_methods_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func assertFailedTestsExpected(f *fuzzerTestContext, expectFailure bool) {
8181
// corpus. It asserts that the actual result matches the provided expected result.
8282
func assertCorpusCallSequencesCollected(f *fuzzerTestContext, expectCallSequences bool) {
8383
// Obtain our count of mutable (often representing just non-reverted coverage increasing) sequences.
84-
callSequenceCount := f.fuzzer.corpus.CallSequenceEntryCount(true, false, false)
84+
callSequenceCount, _ := f.fuzzer.corpus.CallSequenceEntryCount()
8585

8686
// Ensure we captured some coverage-increasing call sequences.
8787
if expectCallSequences {

fuzzing/testdata/contracts/deployments/predeploy_contract.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ contract PredeployContract {
77
contract TestContract {
88
PredeployContract predeploy = PredeployContract(address(0x1234));
99

10+
constructor() payable {}
11+
1012
function testPredeploy() public {
1113
predeploy.triggerFailure();
1214
}

0 commit comments

Comments
 (0)