diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98641bb0..36aade25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,6 @@ jobs: inputs: ./medusa-*.tar.gz - name: Upload artifact - if: github.ref == 'refs/heads/master' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) uses: actions/upload-artifact@v4 with: name: medusa-${{ runner.os }}-${{ runner.arch }} diff --git a/DEV.md b/DEV.md new file mode 100644 index 00000000..7543fe56 --- /dev/null +++ b/DEV.md @@ -0,0 +1,104 @@ +# Debugging and Development + +## Debugging + +The following scripts are available for Medusa developers for debugging changes to the fuzzer. + +### Corpus diff + +The corpus diff script is used to compare two corpora and identify the methods that are present in one but not the other. This is useful for identifying methods that are missing from a corpus that should be present. + +```shell +python3 scripts/corpus_diff.py corpus1 corpus2 +``` + +```shell +Methods only in ~/corpus1: +- clampSplitWeight(uint32,uint32) + +Methods only in ~/corpus2: + +``` + +### Corpus stats + +The corpus stats script is used to generate statistics about a corpus. This includes the number of sequences, the average length of sequences, and the frequency of methods called. + +```shell +python3 scripts/corpus_stats.py corpus +``` + +```shell +Number of Sequences in ~/corpus: 130 + +Average Length of Transactions List: 43 + +Frequency of Methods Called: +- testReceiversReceivedSplit(uint8): 280 +- setMaxEndHints(uint32,uint32): 174 +- setStreamBalanceWithdrawAll(uint8): 139 +- giveClampedAmount(uint8,uint8,uint128): 136 +- receiveStreamsSplitAndCollectToSelf(uint8): 133 +- testSqueezeViewVsActual(uint8,uint8): 128 +- testSqueeze(uint8,uint8): 128 +- testSetStreamBalance(uint8,int128): 128 +- addStreamWithClamping(uint8,uint8,uint160,uint32,uint32,int128): 125 +- removeAllSplits(uint8): 118 +- testSplittableAfterSplit(uint8): 113 +- testSqueezableVsReceived(uint8): 111 +- testBalanceAtInFuture(uint8,uint8,uint160): 108 +- testRemoveStreamShouldNotRevert(uint8,uint256): 103 +- invariantWithdrawAllTokensShouldNotRevert(): 103 +- collect(uint8,uint8): 101 +- invariantAmtPerSecVsMinAmtPerSec(uint8,uint256): 98 +- testSqueezableAmountCantBeWithdrawn(uint8,uint8): 97 +- split(uint8): 97 +- invariantWithdrawAllTokens(): 95 +- testReceiveStreams(uint8,uint32): 93 +- invariantAccountingVsTokenBalance(): 92 +- testSqueezeWithFuzzedHistoryShouldNotRevert(uint8,uint8,uint256,bytes32): 91 +- testSqueezableAmountCantBeUndone(uint8,uint8,uint160,uint32,uint32,int128): 87 +- testCollect(uint8,uint8): 86 +- testSetStreamBalanceWithdrawAllShouldNotRevert(uint8): 86 +- testAddStreamShouldNotRevert(uint8,uint8,uint160,uint32,uint32,int128): 85 +- testReceiveStreamsShouldNotRevert(uint8): 84 +- addSplitsReceiver(uint8,uint8,uint32): 84 +- setStreamBalanceWithClamping(uint8,int128): 82 +- addSplitsReceiverWithClamping(uint8,uint8,uint32): 80 +- testSetStreamBalanceShouldNotRevert(uint8,int128): 80 +- testSplitShouldNotRevert(uint8): 80 +- squeezeAllAndReceiveAndSplitAndCollectToSelf(uint8): 79 +- addStreamImmediatelySqueezable(uint8,uint8,uint160): 79 +- testSetSplitsShouldNotRevert(uint8,uint8,uint32): 78 +- invariantSumAmtDeltaIsZero(uint8): 78 +- testReceiveStreamsViewConsistency(uint8,uint32): 76 +- squeezeToSelf(uint8): 74 +- collectToSelf(uint8): 72 +- setStreams(uint8,uint8,uint160,uint32,uint32,int128): 70 +- receiveStreamsAllCycles(uint8): 69 +- invariantWithdrawShouldAlwaysFail(uint256): 68 +- addStream(uint8,uint8,uint160,uint32,uint32,int128): 68 +- squeezeWithFuzzedHistory(uint8,uint8,uint256,bytes32): 67 +- setStreamsWithClamping(uint8,uint8,uint160,uint32,uint32,int128): 67 +- splitAndCollectToSelf(uint8): 67 +- testSqueezeWithFullyHashedHistory(uint8,uint8): 65 +- give(uint8,uint8,uint128): 65 +- setSplits(uint8,uint8,uint32): 65 +- testSqueezeTwice(uint8,uint8,uint256,bytes32): 65 +- testSetStreamsShouldNotRevert(uint8,uint8,uint160,uint32,uint32,int128): 64 +- squeezeAllSenders(uint8): 63 +- removeStream(uint8,uint256): 62 +- testCollectableAfterSplit(uint8): 58 +- testCollectShouldNotRevert(uint8,uint8): 56 +- testReceiveStreamsViewVsActual(uint8,uint32): 55 +- receiveStreams(uint8,uint32): 55 +- setSplitsWithClamping(uint8,uint8,uint32): 55 +- testGiveShouldNotRevert(uint8,uint8,uint128): 47 +- setStreamBalance(uint8,int128): 47 +- squeezeWithDefaultHistory(uint8,uint8): 45 +- testSplitViewVsActual(uint8): 45 +- testAddSplitsShouldNotRevert(uint8,uint8,uint32): 30 +- testSqueezeWithDefaultHistoryShouldNotRevert(uint8,uint8): 23 + +Number of Unique Methods: 65 +``` diff --git a/docs/src/static/medusa.json b/docs/src/static/medusa.json index 2e8644b6..4e297f54 100644 --- a/docs/src/static/medusa.json +++ b/docs/src/static/medusa.json @@ -7,6 +7,7 @@ "callSequenceLength": 100, "corpusDirectory": "", "coverageEnabled": true, + "experimentalValueGenerationEnabled": true, "targetContracts": [], "targetContractsBalances": [], "constructorArgs": {}, diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index e488238c..836ea573 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -60,6 +60,14 @@ type FuzzingConfig struct { // CoverageEnabled describes whether to use coverage-guided fuzzing CoverageEnabled bool `json:"coverageEnabled"` + // HtmlReportFile describes the name for the html coverage file. If empty, + // the html coverage file will not be saved + HtmlReportFile string `json:"htmlReportPath"` + + // JsonReportFile describes the name for the html coverage file. If empty, + // the json coverage file will not be saved + JsonReportFile string `json:"jsonReportPath"` + // TargetContracts are the target contracts for fuzz testing TargetContracts []string `json:"targetContracts"` @@ -142,6 +150,10 @@ type TestingConfig struct { // OptimizationTesting describes the configuration used for optimization testing. OptimizationTesting OptimizationTestingConfig `json:"optimizationTesting"` + // ExperimentalValueGenerationEnabled describes the configuration used for testing of collection + // and addition of interesting values found during EVM execution to base value set + ExperimentalValueGenerationEnabled bool `json:"experimentalValueGenerationEnabled"` + // TargetFunctionSignatures is a list function signatures call the fuzzer should exclusively target by omitting calls to other signatures. // The signatures should specify the contract name and signature in the ABI format like `Contract.func(uint256,bytes32)`. TargetFunctionSignatures []string `json:"targetFunctionSignatures"` diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 10f45dc1..72e6414d 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -46,6 +46,8 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { ConstructorArgs: map[string]map[string]any{}, CorpusDirectory: "", CoverageEnabled: true, + HtmlReportFile: "coverage_report.html", + JsonReportFile: "coverage_report.json", SenderAddresses: []string{ "0x10000", "0x20000", @@ -64,6 +66,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { TraceAll: false, TargetFunctionSignatures: []string{}, ExcludeFunctionSignatures: []string{}, + ExperimentalValueGenerationEnabled: false, AssertionTesting: AssertionTestingConfig{ Enabled: true, TestViewMethods: false, diff --git a/fuzzing/config/gen_fuzzing_config.go b/fuzzing/config/gen_fuzzing_config.go index 6a2784b7..2f33e1e7 100644 --- a/fuzzing/config/gen_fuzzing_config.go +++ b/fuzzing/config/gen_fuzzing_config.go @@ -23,6 +23,8 @@ func (f FuzzingConfig) MarshalJSON() ([]byte, error) { CallSequenceLength int `json:"callSequenceLength"` CorpusDirectory string `json:"corpusDirectory"` CoverageEnabled bool `json:"coverageEnabled"` + HtmlReportFile string `json:"htmlReportPath"` + JsonReportFile string `json:"jsonReportPath"` TargetContracts []string `json:"targetContracts"` PredeployedContracts map[string]string `json:"predeployedContracts"` TargetContractsBalances []*hexutil.Big `json:"targetContractsBalances"` @@ -45,6 +47,8 @@ func (f FuzzingConfig) MarshalJSON() ([]byte, error) { enc.CallSequenceLength = f.CallSequenceLength enc.CorpusDirectory = f.CorpusDirectory enc.CoverageEnabled = f.CoverageEnabled + enc.HtmlReportFile = f.HtmlReportFile + enc.JsonReportFile = f.JsonReportFile enc.TargetContracts = f.TargetContracts enc.PredeployedContracts = f.PredeployedContracts if f.TargetContractsBalances != nil { @@ -76,6 +80,8 @@ func (f *FuzzingConfig) UnmarshalJSON(input []byte) error { CallSequenceLength *int `json:"callSequenceLength"` CorpusDirectory *string `json:"corpusDirectory"` CoverageEnabled *bool `json:"coverageEnabled"` + HtmlReportFile *string `json:"htmlReportPath"` + JsonReportFile *string `json:"jsonReportPath"` TargetContracts []string `json:"targetContracts"` PredeployedContracts map[string]string `json:"predeployedContracts"` TargetContractsBalances []*hexutil.Big `json:"targetContractsBalances"` @@ -117,6 +123,12 @@ func (f *FuzzingConfig) UnmarshalJSON(input []byte) error { if dec.CoverageEnabled != nil { f.CoverageEnabled = *dec.CoverageEnabled } + if dec.HtmlReportFile != nil { + f.HtmlReportFile = *dec.HtmlReportFile + } + if dec.JsonReportFile != nil { + f.JsonReportFile = *dec.JsonReportFile + } if dec.TargetContracts != nil { f.TargetContracts = dec.TargetContracts } diff --git a/fuzzing/coverage/report_generation.go b/fuzzing/coverage/report_generation.go index c9bc2da1..abe8cc7c 100644 --- a/fuzzing/coverage/report_generation.go +++ b/fuzzing/coverage/report_generation.go @@ -3,41 +3,122 @@ package coverage import ( _ "embed" "fmt" - "github.com/crytic/medusa/compilation/types" - "github.com/crytic/medusa/utils" "html/template" "math" "os" "path/filepath" "strconv" "time" + + "github.com/crytic/medusa/compilation/types" + "github.com/crytic/medusa/utils" ) var ( //go:embed report_template.gohtml htmlReportTemplate []byte + //go:embed report_template.gojson + jsonReportTemplate []byte ) // GenerateReport takes a set of CoverageMaps and compilations, and produces a coverage report using them, detailing // all source mapped ranges of the source files which were covered or not. // Returns an error if one occurred. -func GenerateReport(compilations []types.Compilation, coverageMaps *CoverageMaps, htmlReportPath string) error { +func GenerateReport(compilations []types.Compilation, coverageMaps *CoverageMaps, corpusPath string, htmlReportPath string, jsonReportPath string) error { // Perform source analysis. sourceAnalysis, err := AnalyzeSourceCoverage(compilations, coverageMaps) if err != nil { return err } - // Finally, export the report data we analyzed. + // Stores the output path of the report + var outputPath string + + // Export the html report of the data we analyzed. if htmlReportPath != "" { - err = exportCoverageReport(sourceAnalysis, htmlReportPath) + outputPath = filepath.Join(corpusPath, htmlReportPath) + err = exportHtmlCoverageReport(sourceAnalysis, outputPath) } + // Export the json report of the data we analyzed. + if jsonReportPath != "" { + outputPath = filepath.Join(corpusPath, jsonReportPath) + err2 := exportJsonCoverageReport(sourceAnalysis, outputPath) + if err == nil && err2 != nil { + err = err2 + } + } + return err +} + +// exportCoverageReportJSON takes a previously performed source analysis and generates a JSON coverage report from it. +// Returns an error if one occurs. +func exportJsonCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) error { + functionMap := template.FuncMap{ + "add": func(x int, y int) int { + return x + y + }, + "sub": func(x int, y int) int { + return x - y + }, + "relativePath": func(path string) string { + // Obtain a path relative to our current working directory. + // If we encounter an error, return the original path. + cwd, err := os.Getwd() + if err != nil { + return path + } + relativePath, err := filepath.Rel(cwd, path) + if err != nil { + return path + } + + return relativePath + }, + "lastActiveIndex": func(sourceFileAnalysis *SourceFileAnalysis) int { + // Determine the last active line index and return it + lastIndex := 0 + for lineIndex, line := range sourceFileAnalysis.Lines { + if line.IsActive { + lastIndex = lineIndex + } + } + return lastIndex + }, + } + + // Parse our JSON template + tmpl, err := template.New("coverage_report.json").Funcs(functionMap).Parse(string(jsonReportTemplate)) + if err != nil { + return fmt.Errorf("could not export report, failed to parse report template: %v", err) + } + + // If the parent directory doesn't exist, create it. + parentDirectory := filepath.Dir(outputPath) + err = utils.MakeDirectory(parentDirectory) + if err != nil { + return err + } + + // Create our report file + file, err := os.Create(outputPath) + if err != nil { + _ = file.Close() + return fmt.Errorf("could not export report, failed to open file for writing: %v", err) + } + + // Execute the template and write it back to file. + err = tmpl.Execute(file, sourceAnalysis) + fileCloseErr := file.Close() + if err == nil { + err = fileCloseErr + } + return err } // exportCoverageReport takes a previously performed source analysis and generates an HTML coverage report from it. // Returns an error if one occurs. -func exportCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) error { +func exportHtmlCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) error { // Define mappings onto some useful variables/functions. functionMap := template.FuncMap{ "timeNow": time.Now, @@ -62,9 +143,9 @@ func exportCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) err // Determine our precision string formatStr := "%." + strconv.Itoa(decimals) + "f" - // If no lines are active and none are covered, show 0% coverage + // If no lines are active and none are covered, show 100% coverage if x == 0 && y == 0 { - return fmt.Sprintf(formatStr, float64(0)) + return fmt.Sprintf(formatStr, float64(100)) } return fmt.Sprintf(formatStr, (float64(x)/float64(y))*100) }, diff --git a/fuzzing/coverage/report_template.gojson b/fuzzing/coverage/report_template.gojson new file mode 100644 index 00000000..d0d0b695 --- /dev/null +++ b/fuzzing/coverage/report_template.gojson @@ -0,0 +1,15 @@ +[{{range $index, $sourceFile := .SortedFiles}}{{if $index}},{{end}} +{ +"lines": { +"found": {{$sourceFile.ActiveLineCount}}, +"hit": {{$sourceFile.CoveredLineCount}}, +"details": [{{$lastActive := lastActiveIndex $sourceFile}}{{range $lineIndex, $line := $sourceFile.Lines}}{{if $line.IsActive}} +{ +"line": {{add $lineIndex 1}}, +"hit": {{if or $line.IsCovered $line.IsCoveredReverted}} 1 {{else}} 0 {{end}} +}{{if ne $lineIndex $lastActive}},{{end}}{{end}}{{end}} +] +}, +"file": "{{relativePath $sourceFile.Path}}" +}{{end}} +] diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index 46bfb01a..113aac46 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -1,11 +1,11 @@ package executiontracer import ( + "github.com/crytic/medusa/utils" "math/big" "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/contracts" - "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 960ebfe2..8e42da94 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/ethereum/go-ethereum/crypto" "math/big" "math/rand" "os" @@ -16,6 +15,8 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/crypto" + "github.com/crytic/medusa/fuzzing/executiontracer" "github.com/crytic/medusa/fuzzing/coverage" @@ -813,9 +814,12 @@ func (f *Fuzzer) Start() error { // Finally, generate our coverage report if we have set a valid corpus directory. if err == nil && f.config.Fuzzing.CorpusDirectory != "" { - coverageReportPath := filepath.Join(f.config.Fuzzing.CorpusDirectory, "coverage_report.html") - err = coverage.GenerateReport(f.compilations, f.corpus.CoverageMaps(), coverageReportPath) - f.logger.Info("Coverage report saved to file: ", colors.Bold, coverageReportPath, colors.Reset) + htmlReportPath := f.config.Fuzzing.HtmlReportFile + jsonReportPath := f.config.Fuzzing.JsonReportFile + corpusDir := f.config.Fuzzing.CorpusDirectory + err = coverage.GenerateReport(f.compilations, f.corpus.CoverageMaps(), corpusDir, htmlReportPath, jsonReportPath) + f.logger.Info("HTML Coverage report saved to file: ", colors.Bold, filepath.Join(corpusDir, htmlReportPath), colors.Reset) + f.logger.Info("JSON Coverage report saved to file: ", colors.Bold, filepath.Join(corpusDir, jsonReportPath), colors.Reset) } // Return any encountered error. @@ -846,6 +850,7 @@ func (f *Fuzzer) printMetricsLoop() { // Obtain our metrics callsTested := f.metrics.CallsTested() sequencesTested := f.metrics.SequencesTested() + failedSequences := f.metrics.FailedSequences() workerStartupCount := f.metrics.WorkerStartupCount() workersShrinking := f.metrics.WorkersShrinkingCount() @@ -865,8 +870,9 @@ func (f *Fuzzer) printMetricsLoop() { logBuffer.Append(", calls: ", colors.Bold, fmt.Sprintf("%d (%d/sec)", callsTested, uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate)), colors.Reset) logBuffer.Append(", seq/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate)), colors.Reset) logBuffer.Append(", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset) + logBuffer.Append(", shrinking: ", colors.Bold, fmt.Sprintf("%v", workersShrinking), colors.Reset) + logBuffer.Append(", failures: ", colors.Bold, fmt.Sprintf("%d/%d", failedSequences, sequencesTested), colors.Reset) if f.logger.Level() <= zerolog.DebugLevel { - logBuffer.Append(", shrinking: ", colors.Bold, fmt.Sprintf("%v", workersShrinking), colors.Reset) logBuffer.Append(", mem: ", colors.Bold, fmt.Sprintf("%v/%v MB", memoryUsedMB, memoryTotalMB), colors.Reset) logBuffer.Append(", resets/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate)), colors.Reset) } diff --git a/fuzzing/fuzzer_metrics.go b/fuzzing/fuzzer_metrics.go index 70fc3788..b0984ab0 100644 --- a/fuzzing/fuzzer_metrics.go +++ b/fuzzing/fuzzer_metrics.go @@ -14,6 +14,9 @@ type fuzzerWorkerMetrics struct { // sequencesTested describes the amount of sequences of transactions which tests were run against. sequencesTested *big.Int + //failedSequences describes the amount of sequences of transactions which tests failed. + failedSequences *big.Int + // callsTested describes the amount of transactions/calls the fuzzer executed and ran tests against. callsTested *big.Int @@ -33,12 +36,21 @@ func newFuzzerMetrics(workerCount int) *FuzzerMetrics { } for i := 0; i < len(metrics.workerMetrics); i++ { metrics.workerMetrics[i].sequencesTested = big.NewInt(0) + metrics.workerMetrics[i].failedSequences = big.NewInt(0) metrics.workerMetrics[i].callsTested = big.NewInt(0) metrics.workerMetrics[i].workerStartupCount = big.NewInt(0) } return &metrics } +func (m *FuzzerMetrics) FailedSequences() *big.Int { + failedSequences := big.NewInt(0) + for _, workerMetrics := range m.workerMetrics { + failedSequences.Add(failedSequences, workerMetrics.failedSequences) + } + return failedSequences +} + // SequencesTested returns the amount of sequences of transactions the fuzzer executed and ran tests against. func (m *FuzzerMetrics) SequencesTested() *big.Int { sequencesTested := big.NewInt(0) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 0af56dd9..48e3f2d3 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -936,3 +936,83 @@ func TestExcludeFunctionSignatures(t *testing.T) { } }}) } + +// TestExperimentalValueGeneration runs tests to ensure whether interesting values collected +// during EVM execution is added to the base value set. In addition, it makes sure that the base value set is reset to +// default after the end of each call sequence execution +func TestExperimentalValueGeneration(t *testing.T) { + filePaths := []string{ + "testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol", + } + + for _, filePath := range filePaths { + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: filePath, + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Fuzzing.TestLimit = 500 + config.Fuzzing.Testing.PropertyTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Fuzzing.Workers = 1 + config.Fuzzing.Testing.ExperimentalValueGenerationEnabled = true + }, + method: func(f *fuzzerTestContext) { + valueSet := f.fuzzer.baseValueSet + expectedInts := []int{1, 2, 3, 4} + expectedStrings := []string{"another string", "string"} + expectedAddresses := []common.Address{common.HexToAddress("0x1234"), common.HexToAddress("0x5678")} + expectedByteArrays := [][]byte{[]byte("another byte array"), []byte("byte array"), []byte("word"), []byte("byte")} + + f.fuzzer.Events.WorkerCreated.Subscribe(func(event FuzzerWorkerCreatedEvent) error { + // Wipe constants that were retrieved from AST so that we can test the capturing of values + event.Worker.valueSet = valuegeneration.NewValueSet() + event.Worker.Events.FuzzerWorkerChainSetup.Subscribe(func(event FuzzerWorkerChainSetupEvent) error { + event.Worker.chain.Events.PendingBlockAddedTx.Subscribe(func(event chain.PendingBlockAddedTxEvent) error { + if valueGenerationResults, ok := event.Block.MessageResults[event.TransactionIndex-1].AdditionalResults["ValueGenerationTracerResults"].([]any); ok { + f.fuzzer.workers[0].valueSet.Add(valueGenerationResults) + var contains bool + + for _, intValue := range expectedInts { + contains = valueSet.ContainsInteger(big.NewInt(int64(intValue))) + } + + for _, stringValue := range expectedStrings { + contains = valueSet.ContainsString(stringValue) + } + + for _, addressValue := range expectedAddresses { + contains = valueSet.ContainsAddress(addressValue) + } + + for _, byteArrayValue := range expectedByteArrays { + contains = valueSet.ContainsBytes(byteArrayValue) + } + + assert.True(t, contains) + } + // just check if these values are added to value set + //msgResult[event.TransactionIndex].AdditionalResults + // make sure to use CallSequenceTested event to see if the base value set + // is reset at the end of each sequence + //fmt.Printf("MsgResult: %v\n", msgResult[event.TransactionIndex].AdditionalResults) + return nil + }) + // This will make sure that the base value set is reset after the end of execution of each + // call sequence + event.Worker.Events.CallSequenceTested.Subscribe(func(event FuzzerWorkerCallSequenceTestedEvent) error { + sequenceValueSet := f.fuzzer.baseValueSet + assert.EqualValues(t, valueSet, sequenceValueSet) + return nil + }) + return nil + }) + return nil + }) + err := f.fuzzer.Start() + + assert.NoError(t, err) + + }, + }) + } +} diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 7ac958b2..2f3d3217 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -2,6 +2,7 @@ package fuzzing import ( "fmt" + "github.com/crytic/medusa/fuzzing/valuegenerationtracer" "math/big" "math/rand" @@ -11,7 +12,6 @@ import ( "github.com/crytic/medusa/fuzzing/coverage" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" - "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" "golang.org/x/exp/maps" ) @@ -29,6 +29,10 @@ type FuzzerWorker struct { // coverageTracer describes the tracer used to collect coverage maps during fuzzing campaigns. coverageTracer *coverage.CoverageTracer + // valueGenerationTracer represents the structure that is used for collecting "interesting" values during EVM + // execution, such as emitted event and return values of executed functions in one sequence. + valueGenerationTracer *valuegenerationtracer.ValueGenerationTracer + // testingBaseBlockNumber refers to the block number at which all contracts for testing have been deployed, prior // to any fuzzing activity. This block number is reverted to after testing each call sequence to reset state. testingBaseBlockNumber uint64 @@ -44,9 +48,6 @@ type FuzzerWorker struct { // pureMethods is a list of contract functions which are side-effect free with respect to the EVM (view and/or pure in terms of Solidity mutability). pureMethods []fuzzerTypes.DeployedContractMethod - // methodChooser uses a weighted selection algorithm to choose a method to call, prioritizing state changing methods over pure ones. - methodChooser *randomutils.WeightedRandomChooser[fuzzerTypes.DeployedContractMethod] - // randomProvider provides random data as inputs to decisions throughout the worker. randomProvider *rand.Rand // sequenceGenerator creates entirely new or mutated call sequences based on corpus call sequences, for use in @@ -94,7 +95,6 @@ func newFuzzerWorker(fuzzer *Fuzzer, workerIndex int, randomProvider *rand.Rand) coverageTracer: nil, randomProvider: randomProvider, valueSet: valueSet, - methodChooser: randomutils.NewWeightedRandomChooser[fuzzerTypes.DeployedContractMethod](), } worker.sequenceGenerator = NewCallSequenceGenerator(worker, callSequenceGenConfig) worker.shrinkingValueMutator = shrinkingValueMutator @@ -242,13 +242,13 @@ func (fw *FuzzerWorker) updateMethods() { // If we deployed the contract, also enumerate property tests and state changing methods. for _, method := range contractDefinition.AssertionTestMethods { // Any non-constant method should be tracked as a state changing method. - // We favor calling state changing methods over view/pure methods. if method.IsConstant() { - fw.pureMethods = append(fw.pureMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) - fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(1))) + // Only track the pure/view method if testing view methods is enabled + if fw.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { + fw.pureMethods = append(fw.pureMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) + } } else { fw.stateChangingMethods = append(fw.stateChangingMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) - fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(100))) } } } @@ -260,12 +260,21 @@ func (fw *FuzzerWorker) updateMethods() { // deployed in the Chain. // Returns the length of the call sequence tested, any requests for call sequence shrinking, or an error if one occurs. func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCallSequenceRequest, error) { + // Copy the existing value set if experimental value generation is enabled + var originalValueSet *valuegeneration.ValueSet + if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled { + originalValueSet = fw.valueSet.Clone() + } // After testing the sequence, we'll want to rollback changes to reset our testing state. var err error defer func() { if err == nil { err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber) } + // Reset the value set if experimental value generation is enabled + if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled { + fw.valueSet = originalValueSet + } }() // Initialize a new sequence within our sequence generator. @@ -294,6 +303,16 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall return true, err } + // Add event and return values to the value set if experimental value generation is enabled + if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled { + lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] + + if values, ok := lastExecutedSequenceElement.ChainReference.MessageResults().AdditionalResults["ValueGenerationTracerResults"].([]any); ok { + fw.valueSet.Add(values) + } + + } + // Loop through each test function, signal our worker tested a call, and collect any requests to shrink // this call sequence. for _, callSequenceTestFunc := range fw.fuzzer.Hooks.CallSequenceTestFuncs { @@ -557,6 +576,13 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { fw.coverageTracer = coverage.NewCoverageTracer() initializedChain.AddTracer(fw.coverageTracer.NativeTracer(), true, false) } + + // If we enabled experimental value generation, create a tracer to collect interesting values during EVM + // execution and connect it to the chain + if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled { + fw.valueGenerationTracer = valuegenerationtracer.NewValueGenerationTracer(fw.fuzzer.contractDefinitions) + initializedChain.AddTracer(fw.valueGenerationTracer.NativeTracer(), true, false) + } return nil }) diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index edc3b224..a83aa523 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -2,12 +2,12 @@ package fuzzing import ( "fmt" - "math/big" - "github.com/crytic/medusa/fuzzing/calls" + "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" "github.com/crytic/medusa/utils/randomutils" + "math/big" ) // CallSequenceGenerator generates call sequences iteratively per element, for use in fuzzing campaigns. It is attached @@ -164,6 +164,13 @@ func NewCallSequenceGenerator(worker *FuzzerWorker, config *CallSequenceGenerato }, new(big.Int).SetUint64(config.RandomMutatedCorpusTailWeight), ), + randomutils.NewWeightedRandomChoice( + CallSequenceGeneratorMutationStrategy{ + CallSequenceGeneratorFunc: callSeqGenFuncDuplicateAtRandom, + PrefetchModifyCallFunc: prefetchModifyCallFuncMutate, + }, + new(big.Int).SetUint64(config.RandomMutatedCorpusTailWeight), + ), randomutils.NewWeightedRandomChoice( CallSequenceGeneratorMutationStrategy{ CallSequenceGeneratorFunc: callSeqGenFuncSpliceAtRandom, @@ -274,16 +281,27 @@ func (g *CallSequenceGenerator) PopSequenceElement() (*calls.CallSequenceElement // deployed to the CallSequenceGenerator's parent FuzzerWorker chain, with fuzzed call data. // Returns the call sequence element, or an error if one was encountered. func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement, error) { - // Verify we have state changing methods to call if we are not testing view/pure methods. - if len(g.worker.stateChangingMethods) == 0 && !g.worker.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { - return nil, fmt.Errorf("cannot generate fuzzed tx as there are no state changing methods to call") + // Check to make sure that we have any functions to call + if len(g.worker.stateChangingMethods) == 0 && len(g.worker.pureMethods) == 0 { + return nil, fmt.Errorf("cannot generate fuzzed call as there are no methods to call") } - // Select a random method and sender - selectedMethod, err := g.worker.methodChooser.Choose() - if err != nil { - return nil, err + + // Only call view functions if there are no state-changing methods + var callOnlyPureFunctions bool + if len(g.worker.stateChangingMethods) == 0 && len(g.worker.pureMethods) > 0 { + callOnlyPureFunctions = true } + // Select a random method + // There is a 1/100 chance that a pure method will be invoked or if there are only pure functions that are callable + var selectedMethod *contracts.DeployedContractMethod + if (len(g.worker.pureMethods) > 0 && g.worker.randomProvider.Intn(100) == 0) || callOnlyPureFunctions { + selectedMethod = &g.worker.pureMethods[g.worker.randomProvider.Intn(len(g.worker.pureMethods))] + } else { + selectedMethod = &g.worker.stateChangingMethods[g.worker.randomProvider.Intn(len(g.worker.stateChangingMethods))] + } + + // Select a random sender selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))] // Generate fuzzed parameters for the function call @@ -368,6 +386,22 @@ func callSeqGenFuncCorpusTail(sequenceGenerator *CallSequenceGenerator, sequence return nil } +// callSeqGenFuncDuplicateAtRandom is a CallSequenceGeneratorFunc which prepares a CallSequenceGenerator to generate a sequence +// which duplicates a call sequence element at index N and inserts it at N+1 +// if random index is len(sequence)-1, it inserts the duplicated call sequence element at N-1 +func callSeqGenFuncDuplicateAtRandom(sequenceGenerator *CallSequenceGenerator, sequence calls.CallSequence) error { + randIndex := sequenceGenerator.worker.randomProvider.Intn(len(sequence)) + duplicatedElement := sequence[randIndex] + + if randIndex == len(sequence)-1 { + sequence[randIndex-1] = duplicatedElement + } else { + sequence[randIndex+1] = duplicatedElement + } + + return nil +} + // callSeqGenFuncSpliceAtRandom is a CallSequenceGeneratorFunc which prepares a CallSequenceGenerator to generate a // sequence which is based off of two corpus call sequence entries, from which a random length head and tail are // respectively sliced and joined together. diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index 8ab4a5bd..f9b9978a 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -1,6 +1,7 @@ package fuzzing import ( + "math/big" "sync" "github.com/crytic/medusa/compilation/abiutils" @@ -212,6 +213,7 @@ func (t *AssertionTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorke // Update our test state and report it finalized. testCase.status = TestCaseStatusFailed testCase.callSequence = &shrunkenCallSequence + worker.workerMetrics().failedSequences.Add(worker.workerMetrics().failedSequences, big.NewInt(1)) worker.Fuzzer().ReportTestCaseFinished(testCase) return nil }, diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index 3681d218..6bb6d419 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -332,6 +332,7 @@ func (t *PropertyTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorker testCase.status = TestCaseStatusFailed testCase.callSequence = &shrunkenCallSequence testCase.propertyTestTrace = executionTrace + worker.workerMetrics().failedSequences.Add(worker.workerMetrics().failedSequences, big.NewInt(1)) worker.Fuzzer().ReportTestCaseFinished(testCase) return nil }, diff --git a/fuzzing/testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol b/fuzzing/testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol new file mode 100644 index 00000000..8405cd96 --- /dev/null +++ b/fuzzing/testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.8.0; + +contract AnotherContract { + // This function returns a variety of values that need to be captured in the value set + function testAnotherFunction(uint256 x) public pure returns (uint256, int256, string memory, address, bytes memory, bytes4) { + // Fix the values we want to return + uint256 myUint = 3; + int256 myInt = 4; + string memory myStr = "another string"; + address myAddr = address(0x5678); + bytes memory myBytes = "another byte array"; + bytes4 fixedBytes = "word"; + + return (myUint, myInt, myStr, myAddr, myBytes, fixedBytes); + } +} + +contract TestContract { + AnotherContract public anotherContract; + + event EventValues(uint indexed myUint, int myInt, string myStr, address myAddr, bytes myBytes, bytes4 fixedBytes); + + // Deploy AnotherContract within the TestContract + constructor() { + anotherContract = new AnotherContract(); + } + + function testFunction(uint x) public { + // Fix the values we want to emit + uint256 myUint = 1; + int256 myInt = 2; + string memory myStr = "string"; + address myAddr = address(0x1234); + bytes memory myBytes = "byte array"; + bytes4 fixedBytes = "byte"; + + // Call an external contract + anotherContract.testAnotherFunction(x); + + // Emit an event in this call frame + emit EventValues(myUint, myInt, myStr, myAddr, myBytes, fixedBytes); + + // ASSERTION: We always fail when you call this function. + assert(false); + + } +} diff --git a/fuzzing/valuegeneration/value_set.go b/fuzzing/valuegeneration/value_set.go index 883aab73..3b3cadf2 100644 --- a/fuzzing/valuegeneration/value_set.go +++ b/fuzzing/valuegeneration/value_set.go @@ -172,3 +172,22 @@ func (vs *ValueSet) RemoveBytes(b []byte) { delete(vs.bytes, hashStr) } + +// Add adds one or more values. Note the values could be any primitive type (integer, address, string, bytes) +func (vs *ValueSet) Add(values []any) { + // Iterate across each value and assert on its type + for _, value := range values { + switch v := value.(type) { + case *big.Int: + vs.AddInteger(v) + case common.Address: + vs.AddAddress(v) + case string: + vs.AddString(v) + case []byte: + vs.AddBytes(v) + default: + continue + } + } +} diff --git a/fuzzing/valuegenerationtracer/call_frame.go b/fuzzing/valuegenerationtracer/call_frame.go new file mode 100644 index 00000000..f5ccbb89 --- /dev/null +++ b/fuzzing/valuegenerationtracer/call_frame.go @@ -0,0 +1,67 @@ +package valuegenerationtracer + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// CallFrames represents a list of call frames recorded by the ExecutionTracer. +type CallFrames []*CallFrame + +// CallFrame contains information on each EVM call scope, as recorded by an ExecutionTracer. +type CallFrame struct { + // ToAddress refers to the address which was called by the sender. + ToAddress common.Address + + // ToContractAbi refers to the ABI of the contract which was resolved for the ToAddress. + ToContractAbi *abi.ABI + + // ToInitBytecode refers to the init bytecode recorded for the ToAddress. This is only set if it was being deployed. + ToInitBytecode []byte + + // ToRuntimeBytecode refers to the bytecode recorded for the ToAddress. This is only set if the contract was + // successfully deployed in a previous call or at the end of the current call scope. + ToRuntimeBytecode []byte + + // CodeAddress refers to the address of the code being executed. This can be different from ToAddress if + // a delegate call was made. + CodeAddress common.Address + + // CodeContractAbi refers to the ABI of the contract which was resolved for the CodeAddress. + CodeContractAbi *abi.ABI + + // CodeRuntimeBytecode refers to the bytecode recorded for the CodeAddress. + CodeRuntimeBytecode []byte + + // Logs hold any emitted log events (*types.Log) during this call frame + Logs []*types.Log + + // InputData refers to the message data the EVM call was made with. + InputData []byte + + // ReturnData refers to the data returned by this current call frame. + ReturnData []byte + + // ExecutedCode is a boolean that indicates whether code was executed within a CallFrame. A simple transfer of ETH + // would be an example of a CallFrame where ExecutedCode would be false + ExecutedCode bool + + // ParentCallFrame refers to the call frame which entered this call frame directly. It may be nil if the current + // call frame is a top level call frame. + ParentCallFrame *CallFrame +} + +// IsContractCreation indicates whether a contract creation operation was attempted immediately within this call frame. +// This does not include child or parent frames. +// Returns true if this call frame attempted contract creation. +func (c *CallFrame) IsContractCreation() bool { + return c.ToInitBytecode != nil +} + +// IsProxyCall indicates whether the address the message was sent to, and the address the code is being executed from +// are different. This would be indicative of a delegate call. +// Returns true if the code address and to address do not match, implying a delegate call occurred. +func (c *CallFrame) IsProxyCall() bool { + return c.ToAddress != c.CodeAddress +} diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go new file mode 100644 index 00000000..b1ab50b5 --- /dev/null +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -0,0 +1,234 @@ +package valuegenerationtracer + +import ( + "github.com/crytic/medusa/chain" + "github.com/crytic/medusa/chain/types" + "github.com/crytic/medusa/compilation/abiutils" + "github.com/crytic/medusa/fuzzing/contracts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + coretypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" + "golang.org/x/exp/slices" + "math/big" +) + +// valueGenerationTracerResultsKey describes the key to use when storing tracer results in call message results, or when +// querying them. +const valueGenerationTracerResultsKey = "ValueGenerationTracerResults" + +// ValueGenerationTrace contains information about the values generated during the execution of a given message on the +// EVM. +type ValueGenerationTrace struct { + // transactionOutputValues holds interesting values that were generated during EVM execution + transactionOutputValues []any +} + +// ValueGenerationTracer records value information into a ValueGenerationTrace. It contains information about each +// call frame that was entered and exited, and their associated contract definitions. +type ValueGenerationTracer struct { + // evm refers to the EVM instance last captured. + evmContext *tracing.VMContext + + // trace represents the current execution trace captured by this tracer. + trace *ValueGenerationTrace + + // currentCallFrame references the current call frame being traced. + currentCallFrame *CallFrame + + // contractDefinitions represents the contract definitions to match for execution traces. + contractDefinitions contracts.Contracts + + // nativeTracer is the underlying tracer used to capture EVM execution. + nativeTracer *chain.TestChainTracer +} + +// NativeTracer returns the underlying TestChainTracer. +func (t *ValueGenerationTracer) NativeTracer() *chain.TestChainTracer { + return t.nativeTracer +} + +// NewValueGenerationTracer creates a new ValueGenerationTracer and returns it +func NewValueGenerationTracer(contractDefinitions contracts.Contracts) *ValueGenerationTracer { + tracer := &ValueGenerationTracer{ + contractDefinitions: contractDefinitions, + } + + innerTracer := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: tracer.OnTxStart, + OnEnter: tracer.OnEnter, + OnTxEnd: tracer.OnTxEnd, + OnExit: tracer.OnExit, + OnOpcode: tracer.OnOpcode, + OnLog: tracer.OnLog, + }, + } + tracer.nativeTracer = &chain.TestChainTracer{Tracer: innerTracer, CaptureTxEndSetAdditionalResults: nil} + return tracer +} + +// newValueGenerationTrace creates a new ValueGenerationTrace and returns it +func newValueGenerationTrace() *ValueGenerationTrace { + return &ValueGenerationTrace{ + transactionOutputValues: make([]any, 0), + } +} + +// OnTxStart is called upon the start of transaction execution, as defined by tracers.Tracer. +func (t *ValueGenerationTracer) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transaction, from common.Address) { + t.trace = newValueGenerationTrace() + t.currentCallFrame = nil + // Store our evm reference + t.evmContext = vm +} + +// OnTxEnd is called upon the end of transaction execution, as defined by tracers.Tracer. +func (t *ValueGenerationTracer) OnTxEnd(receipt *coretypes.Receipt, err error) { + +} + +// OnEnter initializes the tracing operation for the top of a call frame, as defined by tracers.Tracer. +func (t *ValueGenerationTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + t.onEnteredCallFrame(to, input, typ == byte(vm.CREATE) || typ == byte(vm.CREATE2), value) +} + +// OnExit is called after a call to finalize tracing completes for the top of a call frame, as defined by tracers.Tracer. +func (t *ValueGenerationTracer) OnExit(depth int, output []byte, used uint64, err error, reverted bool) { + // Update call frame information and capture any emitted event and/or return values from the call frame + t.onExitedCallFrame(output, err) +} + +// OnOpcode records data from an EVM state update, as defined by tracers.Tracer. +func (t *ValueGenerationTracer) OnOpcode(pc uint64, op byte, gas uint64, cost uint64, scope tracing.OpContext, data []byte, depth int, err error) { + // Now that we have executed some code, we have access to the VM scope. From this, we can populate more + // information about our call frame. If this is a delegate or proxy call, the to/code addresses should + // be appropriately represented in this structure. The information populated earlier on frame enter represents + // the raw call data, before delegate transformations are applied, etc. + if !t.currentCallFrame.ExecutedCode { + // This is not always the "to" address, but the current address e.g. for delegatecall. + t.currentCallFrame.ToAddress = scope.Address() + // Mark code as having executed in this scope, so we don't set these values again (as cheat codes may affect it). + // We also want to know if a given call scope executed code, or simply represented a value transfer call. + t.currentCallFrame.ExecutedCode = true + } + + // TODO: look for RET opcode to get runtime values +} + +// OnLog is triggered when a LOG operation is encountered during EVM execution, as defined by tracers.Tracer. +func (t *ValueGenerationTracer) OnLog(log *coretypes.Log) { + // Append log to list of operations for this call frame + t.currentCallFrame.Logs = append(t.currentCallFrame.Logs, log) +} + +// onEnteredCallFrame is a helper method used when a new call frame is entered to record information about it. +func (t *ValueGenerationTracer) onEnteredCallFrame(toAddress common.Address, inputData []byte, isContractCreation bool, value *big.Int) { + // Create our call frame struct to track data for this call frame we entered. + callFrameData := &CallFrame{ + ToAddress: toAddress, + ToContractAbi: nil, + ToInitBytecode: nil, + ToRuntimeBytecode: nil, + CodeAddress: toAddress, // Note: Set temporarily, overwritten if code executes (in CaptureState). + CodeContractAbi: nil, + CodeRuntimeBytecode: nil, + Logs: make([]*coretypes.Log, 0), + InputData: slices.Clone(inputData), + ReturnData: nil, + ExecutedCode: false, + ParentCallFrame: t.currentCallFrame, + } + + // If this is a contract creation, set the init bytecode for this call frame to the input data. + if isContractCreation { + callFrameData.ToInitBytecode = inputData + } + + // Update our current call frame + t.currentCallFrame = callFrameData +} + +// onExitedCallFrame is a helper method used when a call frame is exited, to record information about it. +func (t *ValueGenerationTracer) onExitedCallFrame(output []byte, err error) { + // If this was an initial deployment, now that we're exiting, we'll want to record the finally deployed bytecodes. + if t.currentCallFrame.ToRuntimeBytecode == nil { + // As long as this isn't a failed contract creation, we should be able to fetch "to" byte code on exit. + if !t.currentCallFrame.IsContractCreation() || err == nil { + t.currentCallFrame.ToRuntimeBytecode = t.evmContext.StateDB.GetCode(t.currentCallFrame.ToAddress) + } + } + if t.currentCallFrame.CodeRuntimeBytecode == nil { + // Optimization: If the "to" and "code" addresses match, we can simply set our "code" already fetched "to" + // runtime bytecode. + if t.currentCallFrame.CodeAddress == t.currentCallFrame.ToAddress { + t.currentCallFrame.CodeRuntimeBytecode = t.currentCallFrame.ToRuntimeBytecode + } else { + t.currentCallFrame.CodeRuntimeBytecode = t.evmContext.StateDB.GetCode(t.currentCallFrame.CodeAddress) + } + } + + // Resolve our contract definitions on the call frame data, if they have not been. + t.resolveCallFrameContractDefinitions(t.currentCallFrame) + + // Set return data for this call frame + t.currentCallFrame.ReturnData = slices.Clone(output) + + // Append any event and return values from the call frame only if the code contract ABI is nil + // TODO: Note this won't work if the value/event is returned/emitted from something like a library or cheatcode + codeContractAbi := t.currentCallFrame.CodeContractAbi + if codeContractAbi != nil { + // Append event values. Note that we are appending event values even if an error was thrown + for _, log := range t.currentCallFrame.Logs { + if _, eventInputValues := abiutils.UnpackEventAndValues(codeContractAbi, log); len(eventInputValues) > 0 { + t.trace.transactionOutputValues = append(t.trace.transactionOutputValues, eventInputValues...) + } + } + + // Append return values assuming no error was returned + if method, _ := t.currentCallFrame.CodeContractAbi.MethodById(t.currentCallFrame.InputData); method != nil && err != nil { + if outputValues, decodingError := method.Outputs.Unpack(t.currentCallFrame.ReturnData); decodingError != nil { + t.trace.transactionOutputValues = append(t.trace.transactionOutputValues, outputValues...) + } + } + } + + // We're exiting the current frame, so set our current call frame to the parent + t.currentCallFrame = t.currentCallFrame.ParentCallFrame +} + +// resolveCallFrameContractDefinitions resolves previously unresolved contract definitions for the To and Code addresses +// used within the provided call frame. +func (t *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *CallFrame) { + // Try to resolve contract definitions for "to" address + if callFrame.ToContractAbi == nil { + // Try to resolve definitions from compiled contracts + toContract := t.contractDefinitions.MatchBytecode(callFrame.ToInitBytecode, callFrame.ToRuntimeBytecode) + if toContract != nil { + callFrame.ToContractAbi = &toContract.CompiledContract().Abi + + // If this is a contract creation, set the code address to the address of the contract we just deployed. + if callFrame.IsContractCreation() { + callFrame.CodeContractAbi = &toContract.CompiledContract().Abi + } + } + } + + // Try to resolve contract definitions for "code" address + if callFrame.CodeContractAbi == nil { + codeContract := t.contractDefinitions.MatchBytecode(nil, callFrame.CodeRuntimeBytecode) + if codeContract != nil { + callFrame.CodeContractAbi = &codeContract.CompiledContract().Abi + callFrame.ExecutedCode = true + } + + } +} + +// CaptureTxEndSetAdditionalResults can be used to set additional results captured from execution tracing. If this +// tracer is used during transaction execution (block creation), the results can later be queried from the block. +// This method will only be called on the added tracer if it implements the extended TestChainTracer interface. +func (t *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types.MessageResults) { + results.AdditionalResults[valueGenerationTracerResultsKey] = t.trace.transactionOutputValues +} diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer_test.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer_test.go new file mode 100644 index 00000000..4566d59f --- /dev/null +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer_test.go @@ -0,0 +1 @@ +package valuegenerationtracer diff --git a/go.mod b/go.mod index 05b14e8a..3db36bf2 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,9 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/ethereum/c-kzg-4844 v1.0.2 // indirect github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 // indirect + github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/getsentry/sentry-go v0.28.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -80,8 +82,10 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/mod v0.19.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.23.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect diff --git a/go.sum b/go.sum index d20fe464..9d315e1d 100644 --- a/go.sum +++ b/go.sum @@ -61,12 +61,16 @@ github.com/ethereum/c-kzg-4844 v1.0.2 h1:8tV84BCEiPeOkiVgW9mpYBeBUir2bkCNVqxPwwV github.com/ethereum/c-kzg-4844 v1.0.2/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 h1:KrE8I4reeVvf7C1tm8elRjj4BdscTYzz/WAbYyf/JI4= github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0/go.mod h1:D9AJLVXSyZQXJQVk8oh1EwjISE+sJTn2duYIZC0dy3w= +github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e h1:bBLctRc7kr01YGvaDfgLbTwjFNW5jdp5y5rj8XXBHfY= +github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= @@ -216,6 +220,8 @@ golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJI golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -259,6 +265,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/medusa.json b/medusa.json new file mode 100644 index 00000000..5d6eb81b --- /dev/null +++ b/medusa.json @@ -0,0 +1,87 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "shrinkLimit": 5000, + "callSequenceLength": 100, + "corpusDirectory": "", + "coverageEnabled": true, + "htmlReportPath": "coverage_report.html", + "jsonReportPath": "coverage_report.json", + "targetContracts": [], + "predeployedContracts": {}, + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 125000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": true, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": [ + "optimize_" + ] + }, + "experimentalValueGenerationEnabled": false, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": [] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/scripts/corpus_diff.py b/scripts/corpus_diff.py new file mode 100644 index 00000000..b622f212 --- /dev/null +++ b/scripts/corpus_diff.py @@ -0,0 +1,63 @@ +import os +import json +import sys + +def load_json_files_from_subdirectory(subdirectory): + json_data = [] + for root, _, files in os.walk(subdirectory): + for file in files: + if file.endswith('.json'): + with open(os.path.join(root, file), 'r') as f: + data = json.load(f) + json_data.extend(data) + return json_data + +def extract_unique_methods(transactions): + unique_methods = set() + for tx in transactions: + call_data = tx.get('call', {}) + data_abi_values = call_data.get('dataAbiValues', {}) + method_signature = data_abi_values.get('methodSignature', '') + if method_signature: + unique_methods.add(method_signature) + return unique_methods + +def compare_methods(subdirectory1, subdirectory2): + transactions1 = load_json_files_from_subdirectory(subdirectory1) + transactions2 = load_json_files_from_subdirectory(subdirectory2) + + unique_methods1 = extract_unique_methods(transactions1) + unique_methods2 = extract_unique_methods(transactions2) + + only_in_subdir1 = unique_methods1 - unique_methods2 + only_in_subdir2 = unique_methods2 - unique_methods1 + + return only_in_subdir1, only_in_subdir2 + +def main(subdirectory1, subdirectory2): + + only_in_subdir1, only_in_subdir2 = compare_methods(subdirectory1, subdirectory2) + + print(f"Methods only in {subdirectory1}:") + if len(only_in_subdir1) == 0: + print(" ") + else: + for method in only_in_subdir1: + print(f"- {method}") + print("\n") + + + print(f"Methods only in {subdirectory2}:") + if len(only_in_subdir2) == 0: + print(" ") + else: + for method in only_in_subdir2: + print(f"- {method}") + print("\n") + +if __name__ == '__main__': + if len(sys.argv) != 3: + print("Usage: python3 unique.py ") + print("Compares the unique methods in the two given corpora.") + sys.exit(1) + main(sys.argv[1], sys.argv[2]) diff --git a/scripts/corpus_stats.py b/scripts/corpus_stats.py new file mode 100644 index 00000000..a5c818f8 --- /dev/null +++ b/scripts/corpus_stats.py @@ -0,0 +1,57 @@ +import os +import json +from collections import Counter +import sys + +def load_json_files_from_subdirectory(subdirectory): + json_data = [] + for root, _, files in os.walk(subdirectory): + for file in files: + if file.endswith('.json'): + with open(os.path.join(root, file), 'r') as f: + data = json.load(f) + json_data.append(data) + return json_data + + +def analyze_transactions(transactions, method_counter): + + for tx in transactions: + call_data = tx.get('call', {}) + data_abi_values = call_data.get('dataAbiValues', {}) + method_signature = data_abi_values.get('methodSignature', '') + + method_counter[method_signature] += 1 + + + +def main(subdirectory): + transaction_seqs = load_json_files_from_subdirectory(subdirectory) + + method_counter = Counter() + total_length = 0 + + for seq in transaction_seqs: + analyze_transactions(seq, method_counter) + total_length += len(seq) + + average_length = total_length // len(transaction_seqs) + + print(f"Number of Sequences in {subdirectory}: {len(transaction_seqs)}") + print("\n") + + print(f"Average Length of Transactions List: {average_length}") + print("\n") + print("Frequency of Methods Called:") + for method, count in method_counter.most_common(): + print(f"- {method}: {count}") + print("\n") + print(f"Number of Unique Methods: {len(method_counter)}") + print("\n") + +if __name__ == '__main__': + if len(sys.argv) != 2: + print("Usage: python3 corpus_stats.py ") + print("Computes statistics on the transactions in the given corpus.") + sys.exit(1) + main(sys.argv[1])