From b83bc3f563b080526c7ebd9fc50684ef77432167 Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 09:53:17 -0400 Subject: [PATCH 01/55] Move CallFrame to utils Since this struct is used in multiple files, to avoid dependency cycles, it is moved to utils. --- fuzzing/valuegenerationtracer/valuegeneration_tracer.go | 1 + {fuzzing/executiontracer => utils}/call_frame.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 fuzzing/valuegenerationtracer/valuegeneration_tracer.go rename {fuzzing/executiontracer => utils}/call_frame.go (99%) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go new file mode 100644 index 00000000..4566d59f --- /dev/null +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -0,0 +1 @@ +package valuegenerationtracer diff --git a/fuzzing/executiontracer/call_frame.go b/utils/call_frame.go similarity index 99% rename from fuzzing/executiontracer/call_frame.go rename to utils/call_frame.go index 560a78a6..33b72ea3 100644 --- a/fuzzing/executiontracer/call_frame.go +++ b/utils/call_frame.go @@ -1,4 +1,4 @@ -package executiontracer +package utils import ( "github.com/ethereum/go-ethereum/accounts/abi" From d26cd472a4a6d5a65df0e630e62c39d42ec66913 Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 09:54:48 -0400 Subject: [PATCH 02/55] Add ValueGenerationTracer This tracer is used to get emitted event values and return values of calls and add them to corpus for each sequence. --- .../valuegeneration_tracer.go | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 4566d59f..b5ebba52 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -1 +1,213 @@ package valuegenerationtracer + +import ( + "fmt" + "github.com/crytic/medusa/chain/types" + "github.com/crytic/medusa/compilation/abiutils" + "github.com/crytic/medusa/fuzzing/contracts" + "github.com/crytic/medusa/fuzzing/valuegeneration" + "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + coreTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "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" + +type ValueGenerationTrace struct { + TopLevelCallFrame *utils.CallFrame + + contractDefinitions contracts.Contracts +} + +// TODO: Sanan +type ValueGenerationTracer struct { + // emittedValue describes emitted event values during the execution of the contract. + emittedValues []any + + // functionReturnValues indicates the return value of executed functions in one sequence. + functionReturnValues []any + + // evm refers to the EVM instance last captured. + evm *vm.EVM + + // trace represents the current execution trace captured by this tracer. + trace *ValueGenerationTrace + + // currentCallFrame references the current call frame being traced. + currentCallFrame *utils.CallFrame + + // contractDefinitions represents the contract definitions to match for execution traces. + contractDefinitions contracts.Contracts + + // onNextCaptureState refers to methods which should be executed the next time CaptureState executes. + // CaptureState is called prior to execution of an instruction. This allows actions to be performed + // after some state is captured, on the next state capture (e.g. detecting a log instruction, but + // using this structure to execute code later once the log is committed). + onNextCaptureState []func() +} + +func (v *ValueGenerationTracer) CaptureTxStart(gasLimit uint64) { + // Sanan: start fresh + //v.callDepth = 0 + v.trace = newValueGenerationTrace(v.contractDefinitions) + v.currentCallFrame = nil + v.onNextCaptureState = nil + v.emittedValues = make([]any, 0) + v.functionReturnValues = make([]any, 0) +} + +func (v *ValueGenerationTracer) CaptureTxEnd(restGas uint64) { + //TODO implement me + //panic("implement me") +} + +func (v *ValueGenerationTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + //TODO implement me + v.evm = env + v.captureEnteredCallFrame(from, to, input, create, value) + return + //panic("implement me") +} + +func (v *ValueGenerationTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { + //TODO implement me + //panic("implement me") +} + +func (v *ValueGenerationTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + //TODO implement me +} + +func (v *ValueGenerationTracer) CaptureExit(output []byte, gasUsed uint64, err error) { + //TODO implement me + //panic("implement me") +} + +func newValueGenerationTrace(contracts contracts.Contracts) *ValueGenerationTrace { + return &ValueGenerationTrace{ + TopLevelCallFrame: nil, + contractDefinitions: contracts, + } +} + +// captureEnteredCallFrame is a helper method used when a new call frame is entered to record information about it. +func (v *ValueGenerationTracer) captureEnteredCallFrame(fromAddress common.Address, 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 := &utils.CallFrame{ + SenderAddress: fromAddress, + ToAddress: toAddress, + ToContractName: "", + ToContractAbi: nil, + ToInitBytecode: nil, + ToRuntimeBytecode: nil, + CodeAddress: toAddress, // Note: Set temporarily, overwritten if code executes (in CaptureState). + CodeContractName: "", + CodeContractAbi: nil, + CodeRuntimeBytecode: nil, + Operations: make([]any, 0), + SelfDestructed: false, + InputData: slices.Clone(inputData), + ConstructorArgsData: nil, + ReturnData: nil, + ExecutedCode: false, + CallValue: value, + ReturnError: nil, + ParentCallFrame: v.currentCallFrame, + } + + // If this is a contract creation, set the init bytecode for this call frame to the input data. + if isContractCreation { + callFrameData.ToInitBytecode = inputData + } + + // Set our current call frame in our trace + if v.trace.TopLevelCallFrame == nil { + v.trace.TopLevelCallFrame = callFrameData + } else { + v.currentCallFrame.Operations = append(v.currentCallFrame.Operations, callFrameData) + } + v.currentCallFrame = callFrameData +} +func (v *ValueGenerationTracer) GetEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) { + // Try to unpack our event data + event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) + + if event == nil { + // If we couldn't resolve the event from our immediate contract ABI, it may come from a library. + for _, contract := range v.contractDefinitions { + event, eventInputValues = abiutils.UnpackEventAndValues(&contract.CompiledContract().Abi, eventLog) + if event != nil { + break + } + } + } + + // If we resolved an event definition and unpacked data. + if event != nil { + // Format the values as a comma-separated string + encodedEventValuesString, _ := valuegeneration.EncodeABIArgumentsToString(event.Inputs, eventInputValues) + myEncodedEventValuesString := encodedEventValuesString + fmt.Println(myEncodedEventValuesString) + } + + myEventLogData := eventLog.Data + fmt.Printf("eventLog.Data: %+v\n", myEventLogData) +} + +func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + // Execute all "on next capture state" events and clear them. + for _, eventHandler := range v.onNextCaptureState { + eventHandler() + } + v.onNextCaptureState = nil + + // If a log operation occurred, add a deferred operation to capture it. + if op == vm.LOG0 || op == vm.LOG1 || op == vm.LOG2 || op == vm.LOG3 || op == vm.LOG4 { + v.onNextCaptureState = append(v.onNextCaptureState, func() { + logs := v.evm.StateDB.(*state.StateDB).Logs() + if len(logs) > 0 { + v.currentCallFrame.Operations = append(v.currentCallFrame.Operations, logs[len(logs)-1]) + } + }) + } +} + +func (v *ValueGenerationTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { + //TODO implement me + //panic("implement me") +} +func NewValueGenerationTracer() *ValueGenerationTracer { + fmt.Println("Called NewValueGenerationTracer") + // TODO: Sanan + tracer := &ValueGenerationTracer{ + emittedValues: make([]any, 0), + functionReturnValues: make([]any, 0), + } + return tracer +} + +// 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 (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types.MessageResults) { + // Store our tracer results. + //results.Receipt.Logs = v.currentCallFrame.Operations + //var eventLogs []*coreTypes.Log + for _, operation := range v.currentCallFrame.Operations { + if _, ok := operation.(*utils.CallFrame); ok { + // If this is a call frame being entered, generate information recursively. + fmt.Printf("CallFrame Operation: %+v\n", operation) + } else if eventLog, ok := operation.(*coreTypes.Log); ok { + // If an event log was emitted, add a message for it. + fmt.Printf("Event Operation: %+v\n", eventLog) + v.GetEventsGenerated(v.currentCallFrame, eventLog) + //eventLogs = append(eventLogs, eventLog) + } + } +} From 0024d748a915a938021276f6c63423048ea3129a Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 09:58:05 -0400 Subject: [PATCH 03/55] Import CallFrame from utils for execution_tracer --- fuzzing/executiontracer/execution_trace.go | 13 +++++++------ fuzzing/executiontracer/execution_tracer.go | 9 +++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/fuzzing/executiontracer/execution_trace.go b/fuzzing/executiontracer/execution_trace.go index e85306bf..a4719abc 100644 --- a/fuzzing/executiontracer/execution_trace.go +++ b/fuzzing/executiontracer/execution_trace.go @@ -3,6 +3,7 @@ package executiontracer import ( "encoding/hex" "fmt" + "github.com/crytic/medusa/utils" "regexp" "strings" @@ -22,7 +23,7 @@ import ( type ExecutionTrace struct { // TopLevelCallFrame refers to the root call frame, the first EVM call scope entered when an externally-owned // address calls upon a contract. - TopLevelCallFrame *CallFrame + TopLevelCallFrame *utils.CallFrame // contractDefinitions represents the known contract definitions at the time of tracing. This is used to help // obtain any additional information regarding execution. @@ -41,7 +42,7 @@ func newExecutionTrace(contracts contracts.Contracts) *ExecutionTrace { // This list of elements will hold information about what kind of call it is, wei sent, what method is called, and more. // Additionally, the list may also hold formatting options for console output. This function also returns a non-empty // string in case this call frame represents a call to the console.log precompile contract. -func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) ([]any, string) { +func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *utils.CallFrame) ([]any, string) { // Create list of elements and console log string elements := make([]any, 0) var consoleLogString string @@ -164,7 +165,7 @@ func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) ([ // generateCallFrameExitElements generates a list of elements describing the return data of the call frame (e.g. // traditional return data, assertion failure, revert data, etc.). Additionally, the list may also hold formatting options for console output. -func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *CallFrame) []any { +func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *utils.CallFrame) []any { // Create list of elements elements := make([]any, 0) @@ -250,7 +251,7 @@ func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *CallFrame) []a // generateEventEmittedElements generates a list of elements used to express an event emission. It contains information about an // event log such as the topics and the event data. Additionally, the list may also hold formatting options for console output. -func (t *ExecutionTrace) generateEventEmittedElements(callFrame *CallFrame, eventLog *coreTypes.Log) []any { +func (t *ExecutionTrace) generateEventEmittedElements(callFrame *utils.CallFrame, eventLog *coreTypes.Log) []any { // Create list of elements elements := make([]any, 0) @@ -301,7 +302,7 @@ func (t *ExecutionTrace) generateEventEmittedElements(callFrame *CallFrame, even // generateElementsAndLogsForCallFrame generates a list of elements and logs for a given call frame and its children. // The list of elements may also hold formatting options for console output. The list of logs represent calls to the // console.log precompile contract. -func (t *ExecutionTrace) generateElementsAndLogsForCallFrame(currentDepth int, callFrame *CallFrame) ([]any, []any) { +func (t *ExecutionTrace) generateElementsAndLogsForCallFrame(currentDepth int, callFrame *utils.CallFrame) ([]any, []any) { // Create list of elements and logs elements := make([]any, 0) consoleLogs := make([]any, 0) @@ -334,7 +335,7 @@ func (t *ExecutionTrace) generateElementsAndLogsForCallFrame(currentDepth int, c // Loop for each operation performed in the call frame, to provide a chronological history of operations in the // frame. for _, operation := range callFrame.Operations { - if childCallFrame, ok := operation.(*CallFrame); ok { + if childCallFrame, ok := operation.(*utils.CallFrame); ok { // If this is a call frame being entered, generate information recursively. childOutputLines, childConsoleLogStrings := t.generateElementsAndLogsForCallFrame(currentDepth+1, childCallFrame) elements = append(elements, childOutputLines...) diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index 17ec57fe..c38bfb26 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -1,6 +1,7 @@ package executiontracer import ( + "github.com/crytic/medusa/utils" "math/big" "github.com/crytic/medusa/chain" @@ -45,7 +46,7 @@ type ExecutionTracer struct { trace *ExecutionTrace // currentCallFrame references the current call frame being traced. - currentCallFrame *CallFrame + currentCallFrame *utils.CallFrame // contractDefinitions represents the contract definitions to match for execution traces. contractDefinitions contracts.Contracts @@ -90,7 +91,7 @@ func (t *ExecutionTracer) CaptureTxEnd(restGas uint64) { // resolveConstructorArgs resolves previously unresolved constructor argument ABI data from the call data, if // the call frame provided represents a contract deployment. -func (t *ExecutionTracer) resolveCallFrameConstructorArgs(callFrame *CallFrame, contract *contracts.Contract) { +func (t *ExecutionTracer) resolveCallFrameConstructorArgs(callFrame *utils.CallFrame, contract *contracts.Contract) { // If this is a contract creation and the constructor ABI argument data has not yet been resolved, do so now. if callFrame.ConstructorArgsData == nil && callFrame.IsContractCreation() { // We simply slice the compiled bytecode leading the input data off, and we are left with the constructor @@ -104,7 +105,7 @@ func (t *ExecutionTracer) resolveCallFrameConstructorArgs(callFrame *CallFrame, // resolveCallFrameContractDefinitions resolves previously unresolved contract definitions for the To and Code addresses // used within the provided call frame. -func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *CallFrame) { +func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *utils.CallFrame) { // Try to resolve contract definitions for "to" address if callFrame.ToContractAbi == nil { // Try to resolve definitions from cheat code contracts @@ -150,7 +151,7 @@ func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *CallFra // captureEnteredCallFrame is a helper method used when a new call frame is entered to record information about it. func (t *ExecutionTracer) captureEnteredCallFrame(fromAddress common.Address, 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{ + callFrameData := &utils.CallFrame{ SenderAddress: fromAddress, ToAddress: toAddress, ToContractName: "", From 385b6fb335940701e285c961c3a2f7fe3541e4fa Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 09:59:16 -0400 Subject: [PATCH 04/55] Initialize ValueGenerationTracer in fuzzer_worker.go --- fuzzing/fuzzer_worker.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 7ee4e93a..d6b1ce40 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" @@ -28,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 emitted event values 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 @@ -278,6 +283,8 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall return true, err } + //fw.valueGenerationTracer.GetEventsGenerated() + // 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 { @@ -546,6 +553,12 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { fw.coverageTracer = coverage.NewCoverageTracer() initializedChain.AddTracer(fw.coverageTracer, true, false) } + + if fw.fuzzer.config.Fuzzing.ValueGenerationTracingEnabled { + // TODO: Sanan + fw.valueGenerationTracer = valuegenerationtracer.NewValueGenerationTracer() + initializedChain.AddTracer(fw.valueGenerationTracer, true, false) + } return nil }) From af124088e59f1e4db613aac3e8fbeb4152718beb Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 10:01:14 -0400 Subject: [PATCH 05/55] Add a unit test for ValueGenerationTracer --- fuzzing/fuzzer_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 860ebd93..b6c6fdbc 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -60,6 +60,39 @@ func TestFuzzerHooks(t *testing.T) { }) } +func TestGetEmittedEvents_ValueGeneration(t *testing.T) { + filePaths := []string{ + "testdata/contracts/assertions/assert_immediate.sol", + } + + for _, filePath := range filePaths { + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: filePath, + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TargetContracts = []string{"TestContract"} + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnAssertion = true + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnAllocateTooMuchMemory = true + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnArithmeticUnderflow = true + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnCallUninitializedVariable = true + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnEnumTypeConversionOutOfBounds = true + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnDivideByZero = true + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnIncorrectStorageAccess = true + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnOutOfBoundsArrayAccess = true + //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnPopEmptyArray = true + config.Fuzzing.Testing.PropertyTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false + }, + method: func(f *fuzzerTestContext) { + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + // Check for failed assertion tests. + assertFailedTestsExpected(f, true) + }, + }) + } +} + // TestAssertionMode runs tests to ensure that assertion testing behaves as expected. func TestAssertionMode(t *testing.T) { filePaths := []string{ From faf0952023b30d087ea71f0c72d893463eaebcf7 Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 10:02:09 -0400 Subject: [PATCH 06/55] Enable ValueGenerationTracer in configs --- fuzzing/config/config.go | 2 ++ fuzzing/config/config_defaults.go | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 2fbb1e64..57d48b14 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -59,6 +59,8 @@ type FuzzingConfig struct { // CoverageEnabled describes whether to use coverage-guided fuzzing CoverageEnabled bool `json:"coverageEnabled"` + ValueGenerationTracingEnabled bool `json:"valueGenerationEnabled"` + // TargetContracts are the target contracts for fuzz testing TargetContracts []string `json:"targetContracts"` diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 1fed1120..2d0e4f56 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -33,17 +33,18 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { // Create a project configuration projectConfig := &ProjectConfig{ Fuzzing: FuzzingConfig{ - Workers: 10, - WorkerResetLimit: 50, - Timeout: 0, - TestLimit: 0, - ShrinkLimit: 5_000, - CallSequenceLength: 100, - TargetContracts: []string{}, - TargetContractsBalances: []*big.Int{}, - ConstructorArgs: map[string]map[string]any{}, - CorpusDirectory: "", - CoverageEnabled: true, + Workers: 10, + WorkerResetLimit: 50, + Timeout: 0, + TestLimit: 0, + ShrinkLimit: 5_000, + CallSequenceLength: 100, + TargetContracts: []string{}, + TargetContractsBalances: []*big.Int{}, + ConstructorArgs: map[string]map[string]any{}, + CorpusDirectory: "", + CoverageEnabled: true, + ValueGenerationTracingEnabled: true, // Sanan SenderAddresses: []string{ "0x10000", "0x20000", From 937a7adc1f61cdff743466302054c719c4ec9c44 Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 10:03:43 -0400 Subject: [PATCH 07/55] Enable ValueGenerationTracer in medusa.json --- docs/src/static/medusa.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/static/medusa.json b/docs/src/static/medusa.json index 2e8644b6..961f5b94 100644 --- a/docs/src/static/medusa.json +++ b/docs/src/static/medusa.json @@ -7,6 +7,7 @@ "callSequenceLength": 100, "corpusDirectory": "", "coverageEnabled": true, + "valueGenerationTracingEnabled": true, "targetContracts": [], "targetContractsBalances": [], "constructorArgs": {}, From 39f7d8e49b2bd1d9d153122992070cbb53840ce7 Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 10:04:04 -0400 Subject: [PATCH 08/55] Modify existing contract to fit our test case --- .../contracts/assertions/assert_immediate.sol | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/fuzzing/testdata/contracts/assertions/assert_immediate.sol b/fuzzing/testdata/contracts/assertions/assert_immediate.sol index 956a54e2..16491e39 100644 --- a/fuzzing/testdata/contracts/assertions/assert_immediate.sol +++ b/fuzzing/testdata/contracts/assertions/assert_immediate.sol @@ -1,6 +1,33 @@ +pragma solidity ^0.8.0; + +// This contract includes a function that we will call from TestContract. +contract AnotherContract { + // This function doesn't need to do anything specific for this example. + function externalFunction() public pure returns (string memory) { + return "External function called"; + } +} + // This contract ensures the fuzzer can encounter an immediate assertion failure. contract TestContract { + AnotherContract public anotherContract; + + event ValueReceived(uint indexed value, uint second_val); + event ValueNonIndexedReceived(uint firstval, uint secondval); + + // Deploy AnotherContract within the TestContract + constructor() { + anotherContract = new AnotherContract(); + } + function callingMeFails(uint value) public { + // Call the external function in AnotherContract. + anotherContract.externalFunction(); + uint second_val = 5; + + emit ValueReceived(value, second_val); + emit ValueNonIndexedReceived(1337, 5555); + // ASSERTION: We always fail when you call this function. assert(false); } From 20ef5ce63ffe8489d6138a69ba8f5d9a19b555cb Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 12:16:15 -0400 Subject: [PATCH 09/55] Add necessary functions to get event values --- .../valuegeneration_tracer.go | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index b5ebba52..b0080274 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -1,18 +1,23 @@ package valuegenerationtracer import ( + "encoding/hex" "fmt" + "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/crytic/medusa/fuzzing/valuegeneration" + "github.com/crytic/medusa/logging/colors" "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" coreTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "golang.org/x/exp/slices" "math/big" + "regexp" ) // valueGenerationTracerResultsKey describes the key to use when storing tracer results in call message results, or when @@ -45,6 +50,9 @@ type ValueGenerationTracer struct { // contractDefinitions represents the contract definitions to match for execution traces. contractDefinitions contracts.Contracts + // cheatCodeContracts represents the cheat code contract definitions to match for execution traces. + cheatCodeContracts map[common.Address]*chain.CheatCodeContract + // onNextCaptureState refers to methods which should be executed the next time CaptureState executes. // CaptureState is called prior to execution of an instruction. This allows actions to be performed // after some state is captured, on the next state capture (e.g. detecting a log instruction, but @@ -78,6 +86,7 @@ func (v *ValueGenerationTracer) CaptureStart(env *vm.EVM, from common.Address, t func (v *ValueGenerationTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { //TODO implement me //panic("implement me") + v.captureExitedCallFrame(output, err) } func (v *ValueGenerationTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { @@ -87,6 +96,7 @@ func (v *ValueGenerationTracer) CaptureEnter(typ vm.OpCode, from common.Address, func (v *ValueGenerationTracer) CaptureExit(output []byte, gasUsed uint64, err error) { //TODO implement me //panic("implement me") + v.captureExitedCallFrame(output, err) } func newValueGenerationTrace(contracts contracts.Contracts) *ValueGenerationTrace { @@ -134,6 +144,222 @@ func (v *ValueGenerationTracer) captureEnteredCallFrame(fromAddress common.Addre } v.currentCallFrame = callFrameData } + +// generateCallFrameEnterElements generates a list of elements describing top level information about this call frame. +// This list of elements will hold information about what kind of call it is, wei sent, what method is called, and more. +// Additionally, the list may also hold formatting options for console output. This function also returns a non-empty +// string in case this call frame represents a call to the console.log precompile contract. +func (v *ValueGenerationTracer) generateCallFrameEnterElements(callFrame *utils.CallFrame) ([]any, string) { + // Create list of elements and console log string + elements := make([]any, 0) + var consoleLogString string + + // Define some strings and objects that represent our current call frame + var ( + callType = []any{colors.BlueBold, "[call] ", colors.Reset} + proxyContractName = "" + codeContractName = "" + methodName = "" + method *abi.Method + err error + ) + + // If this is a contract creation or proxy call, use different formatting for call type + if callFrame.IsContractCreation() { + callType = []any{colors.YellowBold, "[creation] ", colors.Reset} + } else if callFrame.IsProxyCall() { + callType = []any{colors.CyanBold, "[proxy call] ", colors.Reset} + } + + // Append the formatted call type information to the list of elements + elements = append(elements, callType...) + + // Resolve our contract names, as well as our method and its name from the code contract. + if callFrame.ToContractAbi != nil { + proxyContractName = callFrame.ToContractName + } + if callFrame.CodeContractAbi != nil { + codeContractName = callFrame.CodeContractName + if callFrame.IsContractCreation() { + methodName = "constructor" + method = &callFrame.CodeContractAbi.Constructor + } else { + method, err = callFrame.CodeContractAbi.MethodById(callFrame.InputData) + if err == nil { + methodName = method.Sig + } + } + } + + // Next we attempt to obtain a display string for the input and output arguments. + var inputArgumentsDisplayText *string + if method != nil { + // Determine what buffer will hold our ABI data. + // - If this a contract deployment, constructor argument data follows code, so we use a different buffer the + // tracer provides. + // - If this is a normal call, the input data for the call is used, with the 32-bit function selector sliced off. + abiDataInputBuffer := make([]byte, 0) + if callFrame.IsContractCreation() { + abiDataInputBuffer = callFrame.ConstructorArgsData + } else if len(callFrame.InputData) >= 4 { + abiDataInputBuffer = callFrame.InputData[4:] + } + + // Unpack our input values and obtain a string to represent them + inputValues, err := method.Inputs.Unpack(abiDataInputBuffer) + if err == nil { + // Encode the ABI arguments into strings + encodedInputString, err := valuegeneration.EncodeABIArgumentsToString(method.Inputs, inputValues) + if err == nil { + inputArgumentsDisplayText = &encodedInputString + } + + // If the call was made to the console log precompile address, let's retrieve the log and format it + if callFrame.ToAddress == chain.ConsoleLogContractAddress { + // First, attempt to do string formatting if the first element is a string, has a percent sign in it, + // and there is at least one argument provided for formatting. + exp := regexp.MustCompile(`%`) + stringInput, isString := inputValues[0].(string) + if isString && exp.MatchString(stringInput) && len(inputValues) > 1 { + // Format the string and add it to the list of logs + consoleLogString = fmt.Sprintf(inputValues[0].(string), inputValues[1:]...) + } else { + // The string does not need to be formatted, and we can just use the encoded input string + consoleLogString = encodedInputString + } + + // Add a bullet point before the string and a new line after the string + if len(consoleLogString) > 0 { + consoleLogString = colors.BULLET_POINT + " " + consoleLogString + "\n" + } + } + } + } + + // If we could not correctly obtain the unpacked arguments in a nice display string (due to not having a resolved + // contract or method definition, or failure to unpack), we display as raw data in the worst case. + if inputArgumentsDisplayText == nil { + temp := fmt.Sprintf("msg_data=%v", hex.EncodeToString(callFrame.InputData)) + inputArgumentsDisplayText = &temp + } + + // Generate the message we wish to output finally, using all these display string components. + // If we executed code, attach additional context such as the contract name, method, etc. + var callInfo string + if callFrame.IsProxyCall() { + if callFrame.ExecutedCode { + callInfo = fmt.Sprintf("%v -> %v.%v(%v) (addr=%v, code=%v, value=%v, sender=%v)", proxyContractName, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CodeAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + } else { + callInfo = fmt.Sprintf("(addr=%v, value=%v, sender=%v)", callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + } + } else { + if callFrame.ExecutedCode { + if callFrame.ToAddress == chain.ConsoleLogContractAddress { + callInfo = fmt.Sprintf("%v.%v(%v)", codeContractName, methodName, *inputArgumentsDisplayText) + } else { + callInfo = fmt.Sprintf("%v.%v(%v) (addr=%v, value=%v, sender=%v)", codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + } + } else { + callInfo = fmt.Sprintf("(addr=%v, value=%v, sender=%v)", callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + } + } + + // Add call information to the elements + elements = append(elements, callInfo, "\n") + + return elements, consoleLogString +} + +// resolveConstructorArgs resolves previously unresolved constructor argument ABI data from the call data, if +// the call frame provided represents a contract deployment. +func (v *ValueGenerationTracer) resolveCallFrameConstructorArgs(callFrame *utils.CallFrame, contract *contracts.Contract) { + // If this is a contract creation and the constructor ABI argument data has not yet been resolved, do so now. + if callFrame.ConstructorArgsData == nil && callFrame.IsContractCreation() { + // We simply slice the compiled bytecode leading the input data off, and we are left with the constructor + // arguments ABI data. + compiledInitBytecode := contract.CompiledContract().InitBytecode + if len(compiledInitBytecode) <= len(callFrame.InputData) { + callFrame.ConstructorArgsData = callFrame.InputData[len(compiledInitBytecode):] + } + } +} + +// resolveCallFrameContractDefinitions resolves previously unresolved contract definitions for the To and Code addresses +// used within the provided call frame. +func (v *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *utils.CallFrame) { + // Try to resolve contract definitions for "to" address + if callFrame.ToContractAbi == nil { + // Try to resolve definitions from cheat code contracts + if cheatCodeContract, ok := v.cheatCodeContracts[callFrame.ToAddress]; ok { + callFrame.ToContractName = cheatCodeContract.Name() + callFrame.ToContractAbi = cheatCodeContract.Abi() + callFrame.ExecutedCode = true + } else { + // Try to resolve definitions from compiled contracts + toContract := v.contractDefinitions.MatchBytecode(callFrame.ToInitBytecode, callFrame.ToRuntimeBytecode) + if toContract != nil { + callFrame.ToContractName = toContract.Name() + callFrame.ToContractAbi = &toContract.CompiledContract().Abi + v.resolveCallFrameConstructorArgs(callFrame, toContract) + + // If this is a contract creation, set the code address to the address of the contract we just deployed. + if callFrame.IsContractCreation() { + callFrame.CodeContractName = toContract.Name() + callFrame.CodeContractAbi = &toContract.CompiledContract().Abi + } + } + } + } + + // Try to resolve contract definitions for "code" address + if callFrame.CodeContractAbi == nil { + // Try to resolve definitions from cheat code contracts + if cheatCodeContract, ok := v.cheatCodeContracts[callFrame.CodeAddress]; ok { + callFrame.CodeContractName = cheatCodeContract.Name() + callFrame.CodeContractAbi = cheatCodeContract.Abi() + callFrame.ExecutedCode = true + } else { + // Try to resolve definitions from compiled contracts + codeContract := v.contractDefinitions.MatchBytecode(nil, callFrame.CodeRuntimeBytecode) + if codeContract != nil { + callFrame.CodeContractName = codeContract.Name() + callFrame.CodeContractAbi = &codeContract.CompiledContract().Abi + } + } + } +} + +// captureExitedCallFrame is a helper method used when a call frame is exited, to record information about it. +func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) { + fmt.Println("Called captureExitedCallFrame") + // If this was an initial deployment, now that we're exiting, we'll want to record the finally deployed bytecodes. + if v.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 !v.currentCallFrame.IsContractCreation() || err == nil { + v.currentCallFrame.ToRuntimeBytecode = v.evm.StateDB.GetCode(v.currentCallFrame.ToAddress) + } + } + if v.currentCallFrame.CodeRuntimeBytecode == nil { + // Optimization: If the "to" and "code" addresses match, we can simply set our "code" already fetched "to" + // runtime bytecode. + if v.currentCallFrame.CodeAddress == v.currentCallFrame.ToAddress { + v.currentCallFrame.CodeRuntimeBytecode = v.currentCallFrame.ToRuntimeBytecode + } else { + v.currentCallFrame.CodeRuntimeBytecode = v.evm.StateDB.GetCode(v.currentCallFrame.CodeAddress) + } + } + + // Resolve our contract definitions on the call frame data, if they have not been. + v.resolveCallFrameContractDefinitions(v.currentCallFrame) + + // Set our information for this call frame + v.currentCallFrame.ReturnData = slices.Clone(output) + v.currentCallFrame.ReturnError = err + + // We're exiting the current frame, so set our current call frame to the parent + v.currentCallFrame = v.currentCallFrame.ParentCallFrame +} + func (v *ValueGenerationTracer) GetEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) { // Try to unpack our event data event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) @@ -161,6 +387,7 @@ func (v *ValueGenerationTracer) GetEventsGenerated(callFrame *utils.CallFrame, e } func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + fmt.Println("Called CaptureState") // Execute all "on next capture state" events and clear them. for _, eventHandler := range v.onNextCaptureState { eventHandler() @@ -208,6 +435,8 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. fmt.Printf("Event Operation: %+v\n", eventLog) v.GetEventsGenerated(v.currentCallFrame, eventLog) //eventLogs = append(eventLogs, eventLog) + results.Receipt.Logs = append(results.Receipt.Logs, eventLog) } } + } From 63cf261a4c31554f6774af9c929e714394715025 Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Tue, 18 Jun 2024 12:28:34 -0400 Subject: [PATCH 10/55] Add captureEnteredCallFrame in CaptureEnter --- fuzzing/valuegenerationtracer/valuegeneration_tracer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index b0080274..dc81add8 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -77,9 +77,9 @@ func (v *ValueGenerationTracer) CaptureTxEnd(restGas uint64) { func (v *ValueGenerationTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { //TODO implement me + fmt.Println("Called CaptureStart") v.evm = env v.captureEnteredCallFrame(from, to, input, create, value) - return //panic("implement me") } @@ -91,6 +91,7 @@ func (v *ValueGenerationTracer) CaptureEnd(output []byte, gasUsed uint64, err er func (v *ValueGenerationTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { //TODO implement me + v.captureEnteredCallFrame(from, to, input, typ == vm.CREATE || typ == vm.CREATE2, value) } func (v *ValueGenerationTracer) CaptureExit(output []byte, gasUsed uint64, err error) { @@ -109,6 +110,7 @@ func newValueGenerationTrace(contracts contracts.Contracts) *ValueGenerationTrac // captureEnteredCallFrame is a helper method used when a new call frame is entered to record information about it. func (v *ValueGenerationTracer) captureEnteredCallFrame(fromAddress common.Address, toAddress common.Address, inputData []byte, isContractCreation bool, value *big.Int) { // Create our call frame struct to track data for this call frame we entered. + fmt.Println("Entered captureEnteredCallFrame") callFrameData := &utils.CallFrame{ SenderAddress: fromAddress, ToAddress: toAddress, From c60029f2dbefd8bb776ebb10e9382194794e4585 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Tue, 18 Jun 2024 13:31:31 -0400 Subject: [PATCH 11/55] grab events --- fuzzing/fuzzer_worker.go | 2 +- .../valuegeneration_tracer.go | 271 +++++------------- 2 files changed, 69 insertions(+), 204 deletions(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index d6b1ce40..a3eca34d 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -556,7 +556,7 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { if fw.fuzzer.config.Fuzzing.ValueGenerationTracingEnabled { // TODO: Sanan - fw.valueGenerationTracer = valuegenerationtracer.NewValueGenerationTracer() + fw.valueGenerationTracer = valuegenerationtracer.NewValueGenerationTracer(fw.fuzzer.contractDefinitions) initializedChain.AddTracer(fw.valueGenerationTracer, true, false) } return nil diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index dc81add8..479e8557 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -1,14 +1,10 @@ package valuegenerationtracer import ( - "encoding/hex" "fmt" - "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/crytic/medusa/fuzzing/valuegeneration" - "github.com/crytic/medusa/logging/colors" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -17,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "golang.org/x/exp/slices" "math/big" - "regexp" ) // valueGenerationTracerResultsKey describes the key to use when storing tracer results in call message results, or when @@ -50,9 +45,6 @@ type ValueGenerationTracer struct { // contractDefinitions represents the contract definitions to match for execution traces. contractDefinitions contracts.Contracts - // cheatCodeContracts represents the cheat code contract definitions to match for execution traces. - cheatCodeContracts map[common.Address]*chain.CheatCodeContract - // onNextCaptureState refers to methods which should be executed the next time CaptureState executes. // CaptureState is called prior to execution of an instruction. This allows actions to be performed // after some state is captured, on the next state capture (e.g. detecting a log instruction, but @@ -60,6 +52,17 @@ type ValueGenerationTracer struct { onNextCaptureState []func() } +func NewValueGenerationTracer(contractDefinitions contracts.Contracts) *ValueGenerationTracer { + fmt.Println("Called NewValueGenerationTracer") + // TODO: Sanan + tracer := &ValueGenerationTracer{ + contractDefinitions: contractDefinitions, + emittedValues: make([]any, 0), + functionReturnValues: make([]any, 0), + } + return tracer +} + func (v *ValueGenerationTracer) CaptureTxStart(gasLimit uint64) { // Sanan: start fresh //v.callDepth = 0 @@ -147,131 +150,6 @@ func (v *ValueGenerationTracer) captureEnteredCallFrame(fromAddress common.Addre v.currentCallFrame = callFrameData } -// generateCallFrameEnterElements generates a list of elements describing top level information about this call frame. -// This list of elements will hold information about what kind of call it is, wei sent, what method is called, and more. -// Additionally, the list may also hold formatting options for console output. This function also returns a non-empty -// string in case this call frame represents a call to the console.log precompile contract. -func (v *ValueGenerationTracer) generateCallFrameEnterElements(callFrame *utils.CallFrame) ([]any, string) { - // Create list of elements and console log string - elements := make([]any, 0) - var consoleLogString string - - // Define some strings and objects that represent our current call frame - var ( - callType = []any{colors.BlueBold, "[call] ", colors.Reset} - proxyContractName = "" - codeContractName = "" - methodName = "" - method *abi.Method - err error - ) - - // If this is a contract creation or proxy call, use different formatting for call type - if callFrame.IsContractCreation() { - callType = []any{colors.YellowBold, "[creation] ", colors.Reset} - } else if callFrame.IsProxyCall() { - callType = []any{colors.CyanBold, "[proxy call] ", colors.Reset} - } - - // Append the formatted call type information to the list of elements - elements = append(elements, callType...) - - // Resolve our contract names, as well as our method and its name from the code contract. - if callFrame.ToContractAbi != nil { - proxyContractName = callFrame.ToContractName - } - if callFrame.CodeContractAbi != nil { - codeContractName = callFrame.CodeContractName - if callFrame.IsContractCreation() { - methodName = "constructor" - method = &callFrame.CodeContractAbi.Constructor - } else { - method, err = callFrame.CodeContractAbi.MethodById(callFrame.InputData) - if err == nil { - methodName = method.Sig - } - } - } - - // Next we attempt to obtain a display string for the input and output arguments. - var inputArgumentsDisplayText *string - if method != nil { - // Determine what buffer will hold our ABI data. - // - If this a contract deployment, constructor argument data follows code, so we use a different buffer the - // tracer provides. - // - If this is a normal call, the input data for the call is used, with the 32-bit function selector sliced off. - abiDataInputBuffer := make([]byte, 0) - if callFrame.IsContractCreation() { - abiDataInputBuffer = callFrame.ConstructorArgsData - } else if len(callFrame.InputData) >= 4 { - abiDataInputBuffer = callFrame.InputData[4:] - } - - // Unpack our input values and obtain a string to represent them - inputValues, err := method.Inputs.Unpack(abiDataInputBuffer) - if err == nil { - // Encode the ABI arguments into strings - encodedInputString, err := valuegeneration.EncodeABIArgumentsToString(method.Inputs, inputValues) - if err == nil { - inputArgumentsDisplayText = &encodedInputString - } - - // If the call was made to the console log precompile address, let's retrieve the log and format it - if callFrame.ToAddress == chain.ConsoleLogContractAddress { - // First, attempt to do string formatting if the first element is a string, has a percent sign in it, - // and there is at least one argument provided for formatting. - exp := regexp.MustCompile(`%`) - stringInput, isString := inputValues[0].(string) - if isString && exp.MatchString(stringInput) && len(inputValues) > 1 { - // Format the string and add it to the list of logs - consoleLogString = fmt.Sprintf(inputValues[0].(string), inputValues[1:]...) - } else { - // The string does not need to be formatted, and we can just use the encoded input string - consoleLogString = encodedInputString - } - - // Add a bullet point before the string and a new line after the string - if len(consoleLogString) > 0 { - consoleLogString = colors.BULLET_POINT + " " + consoleLogString + "\n" - } - } - } - } - - // If we could not correctly obtain the unpacked arguments in a nice display string (due to not having a resolved - // contract or method definition, or failure to unpack), we display as raw data in the worst case. - if inputArgumentsDisplayText == nil { - temp := fmt.Sprintf("msg_data=%v", hex.EncodeToString(callFrame.InputData)) - inputArgumentsDisplayText = &temp - } - - // Generate the message we wish to output finally, using all these display string components. - // If we executed code, attach additional context such as the contract name, method, etc. - var callInfo string - if callFrame.IsProxyCall() { - if callFrame.ExecutedCode { - callInfo = fmt.Sprintf("%v -> %v.%v(%v) (addr=%v, code=%v, value=%v, sender=%v)", proxyContractName, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CodeAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) - } else { - callInfo = fmt.Sprintf("(addr=%v, value=%v, sender=%v)", callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) - } - } else { - if callFrame.ExecutedCode { - if callFrame.ToAddress == chain.ConsoleLogContractAddress { - callInfo = fmt.Sprintf("%v.%v(%v)", codeContractName, methodName, *inputArgumentsDisplayText) - } else { - callInfo = fmt.Sprintf("%v.%v(%v) (addr=%v, value=%v, sender=%v)", codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) - } - } else { - callInfo = fmt.Sprintf("(addr=%v, value=%v, sender=%v)", callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) - } - } - - // Add call information to the elements - elements = append(elements, callInfo, "\n") - - return elements, consoleLogString -} - // resolveConstructorArgs resolves previously unresolved constructor argument ABI data from the call data, if // the call frame provided represents a contract deployment. func (v *ValueGenerationTracer) resolveCallFrameConstructorArgs(callFrame *utils.CallFrame, contract *contracts.Contract) { @@ -291,43 +169,29 @@ func (v *ValueGenerationTracer) resolveCallFrameConstructorArgs(callFrame *utils func (v *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *utils.CallFrame) { // Try to resolve contract definitions for "to" address if callFrame.ToContractAbi == nil { - // Try to resolve definitions from cheat code contracts - if cheatCodeContract, ok := v.cheatCodeContracts[callFrame.ToAddress]; ok { - callFrame.ToContractName = cheatCodeContract.Name() - callFrame.ToContractAbi = cheatCodeContract.Abi() - callFrame.ExecutedCode = true - } else { - // Try to resolve definitions from compiled contracts - toContract := v.contractDefinitions.MatchBytecode(callFrame.ToInitBytecode, callFrame.ToRuntimeBytecode) - if toContract != nil { - callFrame.ToContractName = toContract.Name() - callFrame.ToContractAbi = &toContract.CompiledContract().Abi - v.resolveCallFrameConstructorArgs(callFrame, toContract) - - // If this is a contract creation, set the code address to the address of the contract we just deployed. - if callFrame.IsContractCreation() { - callFrame.CodeContractName = toContract.Name() - callFrame.CodeContractAbi = &toContract.CompiledContract().Abi - } + // Try to resolve definitions from compiled contracts + toContract := v.contractDefinitions.MatchBytecode(callFrame.ToInitBytecode, callFrame.ToRuntimeBytecode) + if toContract != nil { + callFrame.ToContractName = toContract.Name() + callFrame.ToContractAbi = &toContract.CompiledContract().Abi + v.resolveCallFrameConstructorArgs(callFrame, toContract) + + // If this is a contract creation, set the code address to the address of the contract we just deployed. + if callFrame.IsContractCreation() { + callFrame.CodeContractName = toContract.Name() + callFrame.CodeContractAbi = &toContract.CompiledContract().Abi } } } // Try to resolve contract definitions for "code" address if callFrame.CodeContractAbi == nil { - // Try to resolve definitions from cheat code contracts - if cheatCodeContract, ok := v.cheatCodeContracts[callFrame.CodeAddress]; ok { - callFrame.CodeContractName = cheatCodeContract.Name() - callFrame.CodeContractAbi = cheatCodeContract.Abi() - callFrame.ExecutedCode = true - } else { - // Try to resolve definitions from compiled contracts - codeContract := v.contractDefinitions.MatchBytecode(nil, callFrame.CodeRuntimeBytecode) - if codeContract != nil { - callFrame.CodeContractName = codeContract.Name() - callFrame.CodeContractAbi = &codeContract.CompiledContract().Abi - } + codeContract := v.contractDefinitions.MatchBytecode(nil, callFrame.CodeRuntimeBytecode) + if codeContract != nil { + callFrame.CodeContractName = codeContract.Name() + callFrame.CodeContractAbi = &codeContract.CompiledContract().Abi } + } } @@ -362,32 +226,6 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) v.currentCallFrame = v.currentCallFrame.ParentCallFrame } -func (v *ValueGenerationTracer) GetEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) { - // Try to unpack our event data - event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) - - if event == nil { - // If we couldn't resolve the event from our immediate contract ABI, it may come from a library. - for _, contract := range v.contractDefinitions { - event, eventInputValues = abiutils.UnpackEventAndValues(&contract.CompiledContract().Abi, eventLog) - if event != nil { - break - } - } - } - - // If we resolved an event definition and unpacked data. - if event != nil { - // Format the values as a comma-separated string - encodedEventValuesString, _ := valuegeneration.EncodeABIArgumentsToString(event.Inputs, eventInputValues) - myEncodedEventValuesString := encodedEventValuesString - fmt.Println(myEncodedEventValuesString) - } - - myEventLogData := eventLog.Data - fmt.Printf("eventLog.Data: %+v\n", myEventLogData) -} - func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { fmt.Println("Called CaptureState") // Execute all "on next capture state" events and clear them. @@ -411,15 +249,6 @@ func (v *ValueGenerationTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost //TODO implement me //panic("implement me") } -func NewValueGenerationTracer() *ValueGenerationTracer { - fmt.Println("Called NewValueGenerationTracer") - // TODO: Sanan - tracer := &ValueGenerationTracer{ - emittedValues: make([]any, 0), - functionReturnValues: make([]any, 0), - } - return tracer -} // 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. @@ -428,17 +257,53 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. // Store our tracer results. //results.Receipt.Logs = v.currentCallFrame.Operations //var eventLogs []*coreTypes.Log - for _, operation := range v.currentCallFrame.Operations { - if _, ok := operation.(*utils.CallFrame); ok { + events := generateEvents(v.trace.TopLevelCallFrame) + results.AdditionalResults[valueGenerationTracerResultsKey] = events + +} + +func generateEvents(currentCallFrame *utils.CallFrame) []*abi.Event { + events := make([]*abi.Event, 0) + for _, operation := range currentCallFrame.Operations { + if childCallFrame, ok := operation.(*utils.CallFrame); ok { // If this is a call frame being entered, generate information recursively. - fmt.Printf("CallFrame Operation: %+v\n", operation) + generateEvents(childCallFrame) } else if eventLog, ok := operation.(*coreTypes.Log); ok { // If an event log was emitted, add a message for it. fmt.Printf("Event Operation: %+v\n", eventLog) - v.GetEventsGenerated(v.currentCallFrame, eventLog) + events = append(events, getEventsGenerated(currentCallFrame, eventLog)) //eventLogs = append(eventLogs, eventLog) - results.Receipt.Logs = append(results.Receipt.Logs, eventLog) } } + return events +} + +func getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) *abi.Event { + // Try to unpack our event data + //event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) + event, _ := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) + + /*if event == nil { + // If we couldn't resolve the event from our immediate contract ABI, it may come from a library. + for _, contract := range callFrame.contractDefinitions { + event, eventInputValues = abiutils.UnpackEventAndValues(&contract.CompiledContract().Abi, eventLog) + if event != nil { + break + } + } + }*/ + + // If we resolved an event definition and unpacked data. + /*if event != nil { + // Format the values as a comma-separated string + encodedEventValuesString, _ := valuegeneration.EncodeABIArgumentsToString(event.Inputs, eventInputValues) + myEncodedEventValuesString := encodedEventValuesString + fmt.Println(myEncodedEventValuesString) + } + + myEventLogData := eventLog.Data + fmt.Printf("eventLog.Data: %+v\n", myEventLogData)*/ + + return event } From 1695d01b3fa6f125eec4f50d28f2371a43bda542 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Tue, 18 Jun 2024 13:37:03 -0400 Subject: [PATCH 12/55] make generating events a ValueGenerationTrace capability --- .../valuegeneration_tracer.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 479e8557..0a087e0b 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -262,16 +262,16 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. } -func generateEvents(currentCallFrame *utils.CallFrame) []*abi.Event { +func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame) []*abi.Event { events := make([]*abi.Event, 0) for _, operation := range currentCallFrame.Operations { if childCallFrame, ok := operation.(*utils.CallFrame); ok { // If this is a call frame being entered, generate information recursively. - generateEvents(childCallFrame) + t.generateEvents(childCallFrame) } else if eventLog, ok := operation.(*coreTypes.Log); ok { // If an event log was emitted, add a message for it. fmt.Printf("Event Operation: %+v\n", eventLog) - events = append(events, getEventsGenerated(currentCallFrame, eventLog)) + events = append(events, t.getEventsGenerated(currentCallFrame, eventLog)) //eventLogs = append(eventLogs, eventLog) } } @@ -279,20 +279,20 @@ func generateEvents(currentCallFrame *utils.CallFrame) []*abi.Event { return events } -func getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) *abi.Event { +func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) *abi.Event { // Try to unpack our event data //event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) - event, _ := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) + event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) - /*if event == nil { + if event == nil { // If we couldn't resolve the event from our immediate contract ABI, it may come from a library. - for _, contract := range callFrame.contractDefinitions { + for _, contract := range t.contractDefinitions { event, eventInputValues = abiutils.UnpackEventAndValues(&contract.CompiledContract().Abi, eventLog) if event != nil { break } } - }*/ + } // If we resolved an event definition and unpacked data. /*if event != nil { From cdab504980f3002da34648af3bb0e59658791c8b Mon Sep 17 00:00:00 2001 From: s4nsec Date: Fri, 21 Jun 2024 10:09:49 -0400 Subject: [PATCH 13/55] Change emitted event values to check for changes in valueSet Since values inside contracts are added to base value set automatically, it was confusing to see values to be emitted and captured in the value set already. --- fuzzing/testdata/contracts/assertions/assert_immediate.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuzzing/testdata/contracts/assertions/assert_immediate.sol b/fuzzing/testdata/contracts/assertions/assert_immediate.sol index 16491e39..77c057cc 100644 --- a/fuzzing/testdata/contracts/assertions/assert_immediate.sol +++ b/fuzzing/testdata/contracts/assertions/assert_immediate.sol @@ -23,10 +23,10 @@ contract TestContract { function callingMeFails(uint value) public { // Call the external function in AnotherContract. anotherContract.externalFunction(); - uint second_val = 5; + uint second_val = 2+12; emit ValueReceived(value, second_val); - emit ValueNonIndexedReceived(1337, 5555); + emit ValueNonIndexedReceived(111+111, 444+444); // ASSERTION: We always fail when you call this function. assert(false); From 92b769967734cd35023145e7eb86f9e890bf2db6 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Fri, 21 Jun 2024 10:13:01 -0400 Subject: [PATCH 14/55] Store captured event values and their types in a data structure and add them to valueSet This change will make sure to extract captured event values and their types and store them in a data structure. This also makes sure that baseValueSet is cloned at the beginning of each call sequence execution and event values are added to cloned value set. Later, the modified value set is used by fuzzer workers while executing call sequences. --- fuzzing/fuzzer_worker.go | 26 +++++++++- .../valuegeneration_tracer.go | 48 +++++++++++++------ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index a3eca34d..2fc40bf2 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -249,6 +249,9 @@ func (fw *FuzzerWorker) updateStateChangingMethods() { // 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 ValueSet + copyValueSet := fw.ValueSet().Clone() + // After testing the sequence, we'll want to rollback changes to reset our testing state. var err error defer func() { @@ -283,7 +286,21 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall return true, err } - //fw.valueGenerationTracer.GetEventsGenerated() + // Add event values to copied ValueSet + lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] + messageResults := lastExecutedSequenceElement.ChainReference.MessageResults() + valueGenerationTracerResults := messageResults.AdditionalResults["ValueGenerationTracerResults"] + + if eventInputs, ok := valueGenerationTracerResults.([]*valuegenerationtracer.EventInputs); ok { + for _, eventInput := range eventInputs { + if intValue, ok := eventInput.EventValue.(*big.Int); ok { + copyValueSet.AddInteger(intValue) + //fmt.Printf("Event value: %v\n", intValue) + } + } + } + // Set valueSet to copied valueSet + fw.SetValueSet(copyValueSet) // Loop through each test function, signal our worker tested a call, and collect any requests to shrink // this call sequence. @@ -526,6 +543,10 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri return optimizedSequence, err } +func (fw *FuzzerWorker) SetValueSet(valueSet *valuegeneration.ValueSet) { + fw.valueSet = valueSet +} + // run takes a base Chain in a setup state ready for testing, clones it, and begins executing fuzzed transaction calls // and asserting properties are upheld. This runs until Fuzzer.ctx cancels the operation. // Returns a boolean indicating whether Fuzzer.ctx has indicated we cancel the operation, and an error if one occurred. @@ -610,6 +631,9 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { return false, err } + // Set valueSet to the one that includes event values + //fw.SetValueSet(copyValueSet) + // If we have any requests to shrink call sequences, do so now. for _, shrinkVerifier := range shrinkVerifiers { _, err = fw.shrinkCallSequence(callSequence, shrinkVerifier) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 0a087e0b..142121a6 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -6,7 +6,6 @@ import ( "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/utils" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" coreTypes "github.com/ethereum/go-ethereum/core/types" @@ -19,10 +18,16 @@ import ( // querying them. const valueGenerationTracerResultsKey = "ValueGenerationTracerResults" +type EventInputs struct { + EventType string + EventValue any +} + type ValueGenerationTrace struct { TopLevelCallFrame *utils.CallFrame contractDefinitions contracts.Contracts + Events []*EventInputs } // TODO: Sanan @@ -80,7 +85,6 @@ func (v *ValueGenerationTracer) CaptureTxEnd(restGas uint64) { func (v *ValueGenerationTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { //TODO implement me - fmt.Println("Called CaptureStart") v.evm = env v.captureEnteredCallFrame(from, to, input, create, value) //panic("implement me") @@ -197,7 +201,6 @@ func (v *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *u // captureExitedCallFrame is a helper method used when a call frame is exited, to record information about it. func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) { - fmt.Println("Called captureExitedCallFrame") // If this was an initial deployment, now that we're exiting, we'll want to record the finally deployed bytecodes. if v.currentCallFrame.ToRuntimeBytecode == nil { // As long as this isn't a failed contract creation, we should be able to fetch "to" byte code on exit. @@ -227,7 +230,6 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) } func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { - fmt.Println("Called CaptureState") // Execute all "on next capture state" events and clear them. for _, eventHandler := range v.onNextCaptureState { eventHandler() @@ -257,31 +259,37 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. // Store our tracer results. //results.Receipt.Logs = v.currentCallFrame.Operations //var eventLogs []*coreTypes.Log - events := generateEvents(v.trace.TopLevelCallFrame) + //events := make([]EventInputs, 0) + events := v.trace.Events + events = v.trace.generateEvents(v.trace.TopLevelCallFrame, events) results.AdditionalResults[valueGenerationTracerResultsKey] = events } -func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame) []*abi.Event { - events := make([]*abi.Event, 0) - for _, operation := range currentCallFrame.Operations { +func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, events []*EventInputs) []*EventInputs { + //events := make([]*abi.Event, 0) + //events := make([]EventInputs, 0) + for iter, operation := range currentCallFrame.Operations { + fmt.Printf("Iteration: %d\n", iter) if childCallFrame, ok := operation.(*utils.CallFrame); ok { // If this is a call frame being entered, generate information recursively. - t.generateEvents(childCallFrame) + t.generateEvents(childCallFrame, events) } else if eventLog, ok := operation.(*coreTypes.Log); ok { // If an event log was emitted, add a message for it. - fmt.Printf("Event Operation: %+v\n", eventLog) - events = append(events, t.getEventsGenerated(currentCallFrame, eventLog)) + events = append(events, t.getEventsGenerated(currentCallFrame, eventLog)...) + //t.getEventsGenerated(currentCallFrame, eventLog) + fmt.Printf("Value of events: %v\n", events) //eventLogs = append(eventLogs, eventLog) } } - return events } -func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) *abi.Event { +func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) []*EventInputs { // Try to unpack our event data //event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) + //eventData := make(map[string]any)] + eventInputs := t.Events event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) if event == nil { @@ -294,6 +302,18 @@ func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, ev } } + if event != nil { + for name, value := range eventInputValues { + eventInputTypes := event.Inputs[name].Type + eventInput := &EventInputs{ + eventInputTypes.String(), + value, + } + eventInputs = append(eventInputs, eventInput) + fmt.Printf("Event Input Value: %+v\n", value) + } + } + // If we resolved an event definition and unpacked data. /*if event != nil { // Format the values as a comma-separated string @@ -305,5 +325,5 @@ func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, ev myEventLogData := eventLog.Data fmt.Printf("eventLog.Data: %+v\n", myEventLogData)*/ - return event + return eventInputs } From 9118e2119d18651e36c5903de9049bde8d0b1bad Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 11:49:55 -0400 Subject: [PATCH 15/55] Remove SetValueSet function --- fuzzing/fuzzer_worker.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 2fc40bf2..f5ab36f5 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -300,7 +300,7 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall } } // Set valueSet to copied valueSet - fw.SetValueSet(copyValueSet) + fw.valueSet = copyValueSet // Loop through each test function, signal our worker tested a call, and collect any requests to shrink // this call sequence. @@ -543,10 +543,6 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri return optimizedSequence, err } -func (fw *FuzzerWorker) SetValueSet(valueSet *valuegeneration.ValueSet) { - fw.valueSet = valueSet -} - // run takes a base Chain in a setup state ready for testing, clones it, and begins executing fuzzed transaction calls // and asserting properties are upheld. This runs until Fuzzer.ctx cancels the operation. // Returns a boolean indicating whether Fuzzer.ctx has indicated we cancel the operation, and an error if one occurred. From 6c6bdd2c78938778bf2a06f05de7ff51104aaa57 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 17:10:52 -0400 Subject: [PATCH 16/55] Modify assert_immediate.sol to check return value collection functionality --- .../contracts/assertions/assert_immediate.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fuzzing/testdata/contracts/assertions/assert_immediate.sol b/fuzzing/testdata/contracts/assertions/assert_immediate.sol index 77c057cc..7be7059f 100644 --- a/fuzzing/testdata/contracts/assertions/assert_immediate.sol +++ b/fuzzing/testdata/contracts/assertions/assert_immediate.sol @@ -15,14 +15,24 @@ contract TestContract { event ValueReceived(uint indexed value, uint second_val); event ValueNonIndexedReceived(uint firstval, uint secondval); + function internalFunction() public returns (string memory) { + anotherContract.externalFunction(); + return "Internal function called"; + } + // Deploy AnotherContract within the TestContract constructor() { +// internalFunction(); anotherContract = new AnotherContract(); } + function callingMeFails(uint value) public { + // Call internalFunction() + internalFunction(); // Call the external function in AnotherContract. anotherContract.externalFunction(); + uint second_val = 2+12; emit ValueReceived(value, second_val); @@ -30,5 +40,6 @@ contract TestContract { // ASSERTION: We always fail when you call this function. assert(false); + } } From aeabcddfa13a457c521586fe267ddb072b04d4e9 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 17:12:01 -0400 Subject: [PATCH 17/55] Modify fuzzer configs to make debugging easier --- fuzzing/fuzzer_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index b6c6fdbc..e932d40f 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -79,8 +79,11 @@ func TestGetEmittedEvents_ValueGeneration(t *testing.T) { //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnIncorrectStorageAccess = true //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnOutOfBoundsArrayAccess = true //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnPopEmptyArray = true + config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Fuzzing.Testing.StopOnNoTests = false + config.Fuzzing.Workers = 1 }, method: func(f *fuzzerTestContext) { // Start the fuzzer From f23d6085a2915dfb46414238cf73970111d0b839 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 17:13:54 -0400 Subject: [PATCH 18/55] Remove deprecated storage alternatives for event values Since we are using a different structure to store event values and types, we don't need emittedValues and functionReturnValues as members of ValueGenerationTracer structure. --- .../valuegenerationtracer/valuegeneration_tracer.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 142121a6..ff10c76a 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -32,12 +32,6 @@ type ValueGenerationTrace struct { // TODO: Sanan type ValueGenerationTracer struct { - // emittedValue describes emitted event values during the execution of the contract. - emittedValues []any - - // functionReturnValues indicates the return value of executed functions in one sequence. - functionReturnValues []any - // evm refers to the EVM instance last captured. evm *vm.EVM @@ -61,9 +55,7 @@ func NewValueGenerationTracer(contractDefinitions contracts.Contracts) *ValueGen fmt.Println("Called NewValueGenerationTracer") // TODO: Sanan tracer := &ValueGenerationTracer{ - contractDefinitions: contractDefinitions, - emittedValues: make([]any, 0), - functionReturnValues: make([]any, 0), + contractDefinitions: contractDefinitions, } return tracer } @@ -74,8 +66,6 @@ func (v *ValueGenerationTracer) CaptureTxStart(gasLimit uint64) { v.trace = newValueGenerationTrace(v.contractDefinitions) v.currentCallFrame = nil v.onNextCaptureState = nil - v.emittedValues = make([]any, 0) - v.functionReturnValues = make([]any, 0) } func (v *ValueGenerationTracer) CaptureTxEnd(restGas uint64) { From 7f566357b7b68e321575cf7d6b5629dfadff76e2 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 18:52:15 -0400 Subject: [PATCH 19/55] Provide a new structure to store both event and return values TransactionOutputValues structure is used to store event and return values together so that both kinds of values can be provided to results.AdditionalResults[valueGenerationTracerResultsKey]. --- .../valuegenerationtracer/valuegeneration_tracer.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index ff10c76a..77f7e6dd 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -5,7 +5,9 @@ import ( "github.com/crytic/medusa/chain/types" "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/contracts" + "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" coreTypes "github.com/ethereum/go-ethereum/core/types" @@ -23,11 +25,18 @@ type EventInputs struct { EventValue any } +type ReturnData []any + +type TransactionOutputValues struct { + Events []*EventInputs + ReturnValues ReturnData +} + type ValueGenerationTrace struct { TopLevelCallFrame *utils.CallFrame - contractDefinitions contracts.Contracts - Events []*EventInputs + contractDefinitions contracts.Contracts + transactionOutputValues TransactionOutputValues } // TODO: Sanan From 0b2f9e6e4bafa15e8bbcf1378471759eea69454a Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 18:56:29 -0400 Subject: [PATCH 20/55] Get return value of the current call frame --- .../valuegeneration_tracer.go | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 77f7e6dd..903a4a97 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -198,6 +198,36 @@ func (v *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *u } } +// getCallFrameReturnValue generates a list of elements describing the return value of the call frame +func (t *ValueGenerationTracer) getCallFrameReturnValue() string { + // Define some strings that represent our current call frame + var method *abi.Method + + // Resolve our method definition + if t.currentCallFrame.CodeContractAbi != nil { + if t.currentCallFrame.IsContractCreation() { + method = &t.currentCallFrame.CodeContractAbi.Constructor + } else { + method, _ = t.currentCallFrame.CodeContractAbi.MethodById(t.currentCallFrame.InputData) + } + } + + var encodedOutputString string + if method != nil { + // Unpack our output values and obtain a string to represent them, only if we didn't encounter an error. + if t.currentCallFrame.ReturnError == nil { + outputValues, err := method.Outputs.Unpack(t.currentCallFrame.ReturnData) + if err == nil { + encodedOutputString, err = valuegeneration.EncodeABIArgumentsToString(method.Outputs, outputValues) + if err == nil { + } + } + } + } + + return encodedOutputString +} + // captureExitedCallFrame is a helper method used when a call frame is exited, to record information about it. func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) { // If this was an initial deployment, now that we're exiting, we'll want to record the finally deployed bytecodes. From 7d1febf0c740a0898c5144fa123fbd5fa58205b1 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 18:58:06 -0400 Subject: [PATCH 21/55] Call getCallFrameReturnValue from captureExitedCallFrame getCallFrameReturnValue is called from this function as this function itself gets called in CaptureExit and CaptureEnd, allowing us to grab return value of every call frame including the top call frame. --- fuzzing/valuegenerationtracer/valuegeneration_tracer.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 903a4a97..ae2309b4 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -254,6 +254,9 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) v.currentCallFrame.ReturnData = slices.Clone(output) v.currentCallFrame.ReturnError = err + // Grab return data of the call frame + v.trace.transactionOutputValues.ReturnValues = append(v.trace.transactionOutputValues.ReturnValues, v.getCallFrameReturnValue()) + // We're exiting the current frame, so set our current call frame to the parent v.currentCallFrame = v.currentCallFrame.ParentCallFrame } From fc21f10019e69dabd68def5bd35f0c99fe7b5ba1 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 19:00:29 -0400 Subject: [PATCH 22/55] Provide collected event and return values to MessageResults We provide the values to MessageResults so that they can be later on added to the ValueSet of the current call sequence in fuzzer worker. --- .../valuegeneration_tracer.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index ae2309b4..f7214307 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -292,9 +292,20 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. //results.Receipt.Logs = v.currentCallFrame.Operations //var eventLogs []*coreTypes.Log //events := make([]EventInputs, 0) - events := v.trace.Events + + // Collect generated event and return values of the current transaction + events := make([]*EventInputs, 0) events = v.trace.generateEvents(v.trace.TopLevelCallFrame, events) - results.AdditionalResults[valueGenerationTracerResultsKey] = events + returnValues := v.trace.transactionOutputValues.ReturnValues + + v.trace.transactionOutputValues = TransactionOutputValues{ + events, + returnValues, + } + + transactionOutputValues := v.trace.transactionOutputValues + + results.AdditionalResults[valueGenerationTracerResultsKey] = transactionOutputValues } @@ -321,7 +332,7 @@ func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, ev // Try to unpack our event data //event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) //eventData := make(map[string]any)] - eventInputs := t.Events + eventInputs := t.transactionOutputValues.Events event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) if event == nil { From 492e4c4fac746583c93433ab6cda825ca58feb83 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 19:02:01 -0400 Subject: [PATCH 23/55] Write colleced transaction output values(return, event data) to current sequence value set This function will check the type of the collected data and add them accordingly to the value set of the currently executed sequence. --- .../valuegeneration_tracer.go | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index f7214307..95074bf0 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -262,6 +262,7 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) } func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + // TODO: look for RET opcode (for now try getting them from currentCallFrame.ReturnData) // Execute all "on next capture state" events and clear them. for _, eventHandler := range v.onNextCaptureState { eventHandler() @@ -284,6 +285,42 @@ func (v *ValueGenerationTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost //panic("implement me") } +func AddTransactionOutputValuesToValueSet(results *types.MessageResults, copyValueSet *valuegeneration.ValueSet) { + valueGenerationTracerResults := results.AdditionalResults["ValueGenerationTracerResults"] + + if transactionOutputValues, ok := valueGenerationTracerResults.(TransactionOutputValues); ok { + eventValues := transactionOutputValues.Events + returnValues := transactionOutputValues.ReturnValues + + for _, event := range eventValues { + switch v := event.EventValue.(type) { + case *big.Int: + copyValueSet.AddInteger(v) + case common.Address: + copyValueSet.AddAddress(v) + case string: + copyValueSet.AddString(v) + case []byte: + copyValueSet.AddBytes(v) + + } + } + for _, returnValue := range returnValues { + switch v := returnValue.(type) { + case *big.Int: + copyValueSet.AddInteger(v) + case common.Address: + copyValueSet.AddAddress(v) + case string: + copyValueSet.AddString(v) + case []byte: + copyValueSet.AddBytes(v) + + } + } + } +} + // 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. From 0cf6a8614f7e91993519eaf4538a2636f5ee6fec Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 24 Jun 2024 19:03:36 -0400 Subject: [PATCH 24/55] Use AddTransactionOutputValuesToValueSet in fuzzer worker Cleaner code is achieved by migrating the part that is responsible for adding collected data to value set into a separate function. --- fuzzing/fuzzer_worker.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index f5ab36f5..698096f8 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -289,16 +289,8 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall // Add event values to copied ValueSet lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] messageResults := lastExecutedSequenceElement.ChainReference.MessageResults() - valueGenerationTracerResults := messageResults.AdditionalResults["ValueGenerationTracerResults"] + valuegenerationtracer.AddTransactionOutputValuesToValueSet(messageResults, copyValueSet) - if eventInputs, ok := valueGenerationTracerResults.([]*valuegenerationtracer.EventInputs); ok { - for _, eventInput := range eventInputs { - if intValue, ok := eventInput.EventValue.(*big.Int); ok { - copyValueSet.AddInteger(intValue) - //fmt.Printf("Event value: %v\n", intValue) - } - } - } // Set valueSet to copied valueSet fw.valueSet = copyValueSet From 395667eb81216add26658743a563aba984d621b6 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Thu, 27 Jun 2024 12:47:49 -0400 Subject: [PATCH 25/55] Rename ValueGenerationTracingEnabled parameter to ExperimentalValueGenerationEnabled --- docs/src/static/medusa.json | 2 +- fuzzing/config/config.go | 2 +- fuzzing/config/config_defaults.go | 24 ++++++++++++------------ fuzzing/fuzzer_worker.go | 5 +++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/src/static/medusa.json b/docs/src/static/medusa.json index 961f5b94..551ab4e0 100644 --- a/docs/src/static/medusa.json +++ b/docs/src/static/medusa.json @@ -7,7 +7,7 @@ "callSequenceLength": 100, "corpusDirectory": "", "coverageEnabled": true, - "valueGenerationTracingEnabled": true, + "ExperimentalValueGenerationEnabled": true, "targetContracts": [], "targetContractsBalances": [], "constructorArgs": {}, diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 57d48b14..a9bb2358 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -59,7 +59,7 @@ type FuzzingConfig struct { // CoverageEnabled describes whether to use coverage-guided fuzzing CoverageEnabled bool `json:"coverageEnabled"` - ValueGenerationTracingEnabled bool `json:"valueGenerationEnabled"` + ExperimentalValueGenerationEnabled bool `json:"ExperimentalValueGenerationEnabled"` // TargetContracts are the target contracts for fuzz testing TargetContracts []string `json:"targetContracts"` diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 2d0e4f56..79b259e4 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -33,18 +33,18 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { // Create a project configuration projectConfig := &ProjectConfig{ Fuzzing: FuzzingConfig{ - Workers: 10, - WorkerResetLimit: 50, - Timeout: 0, - TestLimit: 0, - ShrinkLimit: 5_000, - CallSequenceLength: 100, - TargetContracts: []string{}, - TargetContractsBalances: []*big.Int{}, - ConstructorArgs: map[string]map[string]any{}, - CorpusDirectory: "", - CoverageEnabled: true, - ValueGenerationTracingEnabled: true, // Sanan + Workers: 10, + WorkerResetLimit: 50, + Timeout: 0, + TestLimit: 0, + ShrinkLimit: 5_000, + CallSequenceLength: 100, + TargetContracts: []string{}, + TargetContractsBalances: []*big.Int{}, + ConstructorArgs: map[string]map[string]any{}, + CorpusDirectory: "", + CoverageEnabled: true, + ExperimentalValueGenerationEnabled: true, // Sanan SenderAddresses: []string{ "0x10000", "0x20000", diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 698096f8..b04c1e01 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -563,8 +563,9 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { initializedChain.AddTracer(fw.coverageTracer, true, false) } - if fw.fuzzer.config.Fuzzing.ValueGenerationTracingEnabled { - // TODO: Sanan + // 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.ExperimentalValueGenerationEnabled { fw.valueGenerationTracer = valuegenerationtracer.NewValueGenerationTracer(fw.fuzzer.contractDefinitions) initializedChain.AddTracer(fw.valueGenerationTracer, true, false) } From 01c1de3ca93216701e9507db89c6f454193a3f3f Mon Sep 17 00:00:00 2001 From: s4nsec Date: Thu, 27 Jun 2024 12:48:57 -0400 Subject: [PATCH 26/55] Modify the logic to preserve base value set --- fuzzing/fuzzer_worker.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index b04c1e01..8a6be636 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -29,8 +29,8 @@ 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 emitted event values and return - // values of executed functions in one sequence. + // 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 @@ -250,7 +250,7 @@ func (fw *FuzzerWorker) updateStateChangingMethods() { // 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 ValueSet - copyValueSet := fw.ValueSet().Clone() + originalValueSet := fw.ValueSet().Clone() // After testing the sequence, we'll want to rollback changes to reset our testing state. var err error @@ -258,6 +258,7 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall if err == nil { err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber) } + fw.valueSet = originalValueSet }() // Initialize a new sequence within our sequence generator. @@ -289,10 +290,7 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall // Add event values to copied ValueSet lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] messageResults := lastExecutedSequenceElement.ChainReference.MessageResults() - valuegenerationtracer.AddTransactionOutputValuesToValueSet(messageResults, copyValueSet) - - // Set valueSet to copied valueSet - fw.valueSet = copyValueSet + valuegenerationtracer.AddTransactionOutputValuesToValueSet(messageResults, fw.valueSet) // Loop through each test function, signal our worker tested a call, and collect any requests to shrink // this call sequence. From ba1c045cbcdb6761908f0afe9bde23b61ea4d322 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Thu, 27 Jun 2024 12:51:18 -0400 Subject: [PATCH 27/55] Set TransactionOutputValues to []any As we don't need to differentiate between an emitted value and a return value, and also saving the type of emitted values are not necessary, we can simply use a slice of any elements to store interesting elements found during EVM execution. --- .../valuegenerationtracer/valuegeneration_tracer.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 95074bf0..17bd4b09 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -20,17 +20,7 @@ import ( // querying them. const valueGenerationTracerResultsKey = "ValueGenerationTracerResults" -type EventInputs struct { - EventType string - EventValue any -} - -type ReturnData []any - -type TransactionOutputValues struct { - Events []*EventInputs - ReturnValues ReturnData -} +type TransactionOutputValues []any type ValueGenerationTrace struct { TopLevelCallFrame *utils.CallFrame From aeef26841d1f5068c770068b4f1ff35544d873a3 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Thu, 27 Jun 2024 12:53:24 -0400 Subject: [PATCH 28/55] Perform modifications on ValueGenerationTracer methods to adapt to new structure for holding emitted and return values --- .../valuegeneration_tracer.go | 96 ++++++------------- 1 file changed, 28 insertions(+), 68 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 17bd4b09..72bf130b 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -189,10 +189,13 @@ func (v *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *u } // getCallFrameReturnValue generates a list of elements describing the return value of the call frame -func (t *ValueGenerationTracer) getCallFrameReturnValue() string { +func (t *ValueGenerationTracer) getCallFrameReturnValue() TransactionOutputValues { // Define some strings that represent our current call frame var method *abi.Method + // Define a slice of any to represent return values of the current call frame + var outputValues TransactionOutputValues + // Resolve our method definition if t.currentCallFrame.CodeContractAbi != nil { if t.currentCallFrame.IsContractCreation() { @@ -202,20 +205,15 @@ func (t *ValueGenerationTracer) getCallFrameReturnValue() string { } } - var encodedOutputString string if method != nil { // Unpack our output values and obtain a string to represent them, only if we didn't encounter an error. if t.currentCallFrame.ReturnError == nil { - outputValues, err := method.Outputs.Unpack(t.currentCallFrame.ReturnData) - if err == nil { - encodedOutputString, err = valuegeneration.EncodeABIArgumentsToString(method.Outputs, outputValues) - if err == nil { - } - } + outputValue, _ := method.Outputs.Unpack(t.currentCallFrame.ReturnData) + outputValues = append(outputValues, outputValue) } } - return encodedOutputString + return outputValues } // captureExitedCallFrame is a helper method used when a call frame is exited, to record information about it. @@ -245,7 +243,7 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) v.currentCallFrame.ReturnError = err // Grab return data of the call frame - v.trace.transactionOutputValues.ReturnValues = append(v.trace.transactionOutputValues.ReturnValues, v.getCallFrameReturnValue()) + v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, v.getCallFrameReturnValue()) // We're exiting the current frame, so set our current call frame to the parent v.currentCallFrame = v.currentCallFrame.ParentCallFrame @@ -275,40 +273,29 @@ func (v *ValueGenerationTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost //panic("implement me") } -func AddTransactionOutputValuesToValueSet(results *types.MessageResults, copyValueSet *valuegeneration.ValueSet) { +func AddTransactionOutputValuesToValueSet(results *types.MessageResults, valueSet *valuegeneration.ValueSet) { valueGenerationTracerResults := results.AdditionalResults["ValueGenerationTracerResults"] if transactionOutputValues, ok := valueGenerationTracerResults.(TransactionOutputValues); ok { - eventValues := transactionOutputValues.Events - returnValues := transactionOutputValues.ReturnValues - for _, event := range eventValues { - switch v := event.EventValue.(type) { - case *big.Int: - copyValueSet.AddInteger(v) - case common.Address: - copyValueSet.AddAddress(v) - case string: - copyValueSet.AddString(v) - case []byte: - copyValueSet.AddBytes(v) - - } - } - for _, returnValue := range returnValues { - switch v := returnValue.(type) { + for _, eventReturnValue := range transactionOutputValues { + switch v := eventReturnValue.(type) { case *big.Int: - copyValueSet.AddInteger(v) + valueSet.AddInteger(v) case common.Address: - copyValueSet.AddAddress(v) + valueSet.AddAddress(v) case string: - copyValueSet.AddString(v) + valueSet.AddString(v) case []byte: - copyValueSet.AddBytes(v) + valueSet.AddBytes(v) + default: + continue } } } + + fmt.Printf("ValueSet after modification: %v\n", valueSet) } // CaptureTxEndSetAdditionalResults can be used to set additional results captured from execution tracing. If this @@ -321,24 +308,16 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. //events := make([]EventInputs, 0) // Collect generated event and return values of the current transaction - events := make([]*EventInputs, 0) - events = v.trace.generateEvents(v.trace.TopLevelCallFrame, events) - returnValues := v.trace.transactionOutputValues.ReturnValues - - v.trace.transactionOutputValues = TransactionOutputValues{ - events, - returnValues, - } + eventAndReturnValues := make(TransactionOutputValues, 0) + eventAndReturnValues = v.trace.generateEvents(v.trace.TopLevelCallFrame, eventAndReturnValues) - transactionOutputValues := v.trace.transactionOutputValues + v.trace.transactionOutputValues = eventAndReturnValues - results.AdditionalResults[valueGenerationTracerResultsKey] = transactionOutputValues + results.AdditionalResults[valueGenerationTracerResultsKey] = v.trace.transactionOutputValues } -func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, events []*EventInputs) []*EventInputs { - //events := make([]*abi.Event, 0) - //events := make([]EventInputs, 0) +func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, events TransactionOutputValues) TransactionOutputValues { for iter, operation := range currentCallFrame.Operations { fmt.Printf("Iteration: %d\n", iter) if childCallFrame, ok := operation.(*utils.CallFrame); ok { @@ -355,11 +334,9 @@ func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, return events } -func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) []*EventInputs { +func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) TransactionOutputValues { // Try to unpack our event data - //event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) - //eventData := make(map[string]any)] - eventInputs := t.transactionOutputValues.Events + eventInputs := t.transactionOutputValues event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) if event == nil { @@ -373,27 +350,10 @@ func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, ev } if event != nil { - for name, value := range eventInputValues { - eventInputTypes := event.Inputs[name].Type - eventInput := &EventInputs{ - eventInputTypes.String(), - value, - } - eventInputs = append(eventInputs, eventInput) - fmt.Printf("Event Input Value: %+v\n", value) + for _, value := range eventInputValues { + eventInputs = append(eventInputs, value) } } - // If we resolved an event definition and unpacked data. - /*if event != nil { - // Format the values as a comma-separated string - encodedEventValuesString, _ := valuegeneration.EncodeABIArgumentsToString(event.Inputs, eventInputValues) - myEncodedEventValuesString := encodedEventValuesString - fmt.Println(myEncodedEventValuesString) - } - - myEventLogData := eventLog.Data - fmt.Printf("eventLog.Data: %+v\n", myEventLogData)*/ - return eventInputs } From be7c5b10a4b0f342a50b4c973fb17cc78e3c7da5 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Thu, 27 Jun 2024 12:55:37 -0400 Subject: [PATCH 29/55] Emit and return values of various types Random value generator functions are used to generate values during runtime as constants in the contract are already extracted and added to base value set via AST parsing. --- .../contracts/assertions/assert_immediate.sol | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/fuzzing/testdata/contracts/assertions/assert_immediate.sol b/fuzzing/testdata/contracts/assertions/assert_immediate.sol index 7be7059f..29cd1f56 100644 --- a/fuzzing/testdata/contracts/assertions/assert_immediate.sol +++ b/fuzzing/testdata/contracts/assertions/assert_immediate.sol @@ -12,12 +12,45 @@ contract AnotherContract { contract TestContract { AnotherContract public anotherContract; - event ValueReceived(uint indexed value, uint second_val); - event ValueNonIndexedReceived(uint firstval, uint secondval); + bytes private constant _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - function internalFunction() public returns (string memory) { - anotherContract.externalFunction(); - return "Internal function called"; + function random(uint seed) internal view returns (uint) { + return uint(keccak256(abi.encodePacked(block.timestamp, block.difficulty, seed))); + } + + function generateRandomString(uint seed) public view returns (string memory) { + bytes memory result = new bytes(6); + uint rand = random(seed); + + for (uint i = 0; i < 6; i++) { + result[i] = _chars[rand % _chars.length]; + rand = rand / _chars.length; + } + + return string(result); + } + + function generateRandomByteArray(uint seed, uint length) public view returns (bytes memory) { + bytes memory result = new bytes(length); + uint rand = random(seed); + + for (uint i = 0; i < length; i++) { + result[i] = _chars[rand % _chars.length]; + rand = rand / _chars.length; + } + + return result; + } + + event ValueReceived(uint indexed value, uint second_val, string test, bytes byteArray); + event ValueNonIndexedReceived(uint firstval, uint secondval, bytes1 myByte); + + function internalFunction() public returns (string memory, string memory, uint, bytes1, bytes memory) { + string memory internalString = generateRandomString(444); + uint randInt = 4+14; + bytes1 randByte = 0x44; + bytes memory randBytes = generateRandomByteArray(555, 11); + return ("Internal function called", internalString, randInt, randByte, randBytes); } // Deploy AnotherContract within the TestContract @@ -33,10 +66,15 @@ contract TestContract { // Call the external function in AnotherContract. anotherContract.externalFunction(); + string memory randString = generateRandomString(123); + bytes memory byteArray = generateRandomByteArray(456, 10); + uint second_val = 2+12; - emit ValueReceived(value, second_val); - emit ValueNonIndexedReceived(111+111, 444+444); + bytes1 myByte = 0x51; + + emit ValueReceived(value, second_val, randString, byteArray); + emit ValueNonIndexedReceived(111+111, 444+444, myByte); // ASSERTION: We always fail when you call this function. assert(false); From f8874e70edbbb416ae3ea21cfb581dae493793d9 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Thu, 27 Jun 2024 15:50:14 -0400 Subject: [PATCH 30/55] Add a test to check if base value set is preserved throughout the fuzzing campaign --- fuzzing/fuzzer_test.go | 30 +++++++++++++++++++ .../valuegeneration_tracer_test.go | 1 + 2 files changed, 31 insertions(+) create mode 100644 fuzzing/valuegenerationtracer/valuegeneration_tracer_test.go diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index e932d40f..e122413d 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -60,6 +60,36 @@ func TestFuzzerHooks(t *testing.T) { }) } +func TestExperimentalValueGeneration_ValueSet(t *testing.T) { + filePaths := []string{ + "testdata/contracts/assertions/assert_immediate.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.AssertionTesting.Enabled = false + config.Fuzzing.Testing.PropertyTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Fuzzing.Testing.StopOnNoTests = false + config.Fuzzing.Workers = 1 + }, + method: func(f *fuzzerTestContext) { + baseValueSet := f.fuzzer.baseValueSet + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + + valueSet := f.fuzzer.baseValueSet + + assert.EqualValues(t, valueSet, baseValueSet) + }, + }) + } +} func TestGetEmittedEvents_ValueGeneration(t *testing.T) { filePaths := []string{ "testdata/contracts/assertions/assert_immediate.sol", 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 From 233630dc414ec2985fd78e98f7ab8873130c72ba Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 1 Jul 2024 11:35:56 -0400 Subject: [PATCH 31/55] Move ExperimantalValueGenerationEnabled to TestingConfig --- fuzzing/config/config.go | 6 ++++-- fuzzing/config/config_defaults.go | 2 +- fuzzing/fuzzer_test.go | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index a9bb2358..796b43be 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -59,8 +59,6 @@ type FuzzingConfig struct { // CoverageEnabled describes whether to use coverage-guided fuzzing CoverageEnabled bool `json:"coverageEnabled"` - ExperimentalValueGenerationEnabled bool `json:"ExperimentalValueGenerationEnabled"` - // TargetContracts are the target contracts for fuzz testing TargetContracts []string `json:"targetContracts"` @@ -138,6 +136,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"` } // AssertionTestingConfig describes the configuration options used for assertion testing diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 79b259e4..4ae6568f 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -44,7 +44,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { ConstructorArgs: map[string]map[string]any{}, CorpusDirectory: "", CoverageEnabled: true, - ExperimentalValueGenerationEnabled: true, // Sanan + ExperimentalValueGenerationEnabled: false, SenderAddresses: []string{ "0x10000", "0x20000", diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index e122413d..77ee9ace 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -76,6 +76,7 @@ func TestExperimentalValueGeneration_ValueSet(t *testing.T) { config.Fuzzing.Testing.OptimizationTesting.Enabled = false config.Fuzzing.Testing.StopOnNoTests = false config.Fuzzing.Workers = 1 + config.Fuzzing.Testing.ExperimentalValueGenerationEnabled = true }, method: func(f *fuzzerTestContext) { baseValueSet := f.fuzzer.baseValueSet From 5c55b7f65dea9cfbe4489701f03a66ec1eccacb4 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 1 Jul 2024 11:36:51 -0400 Subject: [PATCH 32/55] Fix overwriting return values with event values issue --- fuzzing/valuegenerationtracer/valuegeneration_tracer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 72bf130b..57f15a70 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -311,7 +311,7 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. eventAndReturnValues := make(TransactionOutputValues, 0) eventAndReturnValues = v.trace.generateEvents(v.trace.TopLevelCallFrame, eventAndReturnValues) - v.trace.transactionOutputValues = eventAndReturnValues + v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, eventAndReturnValues) results.AdditionalResults[valueGenerationTracerResultsKey] = v.trace.transactionOutputValues From 84dd114fa063d0fe78f9746fbc1cfc7f1e637a6f Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 1 Jul 2024 11:37:13 -0400 Subject: [PATCH 33/55] Use eventOrReturnValue as variable name to avoid ambiguity --- fuzzing/valuegenerationtracer/valuegeneration_tracer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 57f15a70..e74c0391 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -278,8 +278,8 @@ func AddTransactionOutputValuesToValueSet(results *types.MessageResults, valueSe if transactionOutputValues, ok := valueGenerationTracerResults.(TransactionOutputValues); ok { - for _, eventReturnValue := range transactionOutputValues { - switch v := eventReturnValue.(type) { + for _, eventOrReturnValue := range transactionOutputValues { + switch v := eventOrReturnValue.(type) { case *big.Int: valueSet.AddInteger(v) case common.Address: From cb7d04fa1674d37a3c191aea4f5df98c0452b6a5 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 1 Jul 2024 11:37:34 -0400 Subject: [PATCH 34/55] Grab only if ReturnError is nil --- fuzzing/valuegenerationtracer/valuegeneration_tracer.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index e74c0391..09653c6f 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -242,8 +242,10 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) v.currentCallFrame.ReturnData = slices.Clone(output) v.currentCallFrame.ReturnError = err - // Grab return data of the call frame - v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, v.getCallFrameReturnValue()) + if v.currentCallFrame.ReturnError == nil { + // Grab return data of the call frame + v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, v.getCallFrameReturnValue()) + } // We're exiting the current frame, so set our current call frame to the parent v.currentCallFrame = v.currentCallFrame.ParentCallFrame From b1c3d0f5d0d94638df8eec92779826a6fbaeb898 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 1 Jul 2024 11:38:15 -0400 Subject: [PATCH 35/55] Don't expect values when it is contract creation --- fuzzing/fuzzer_worker.go | 3 --- fuzzing/valuegenerationtracer/valuegeneration_tracer.go | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 8a6be636..71c7540e 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -618,9 +618,6 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { return false, err } - // Set valueSet to the one that includes event values - //fw.SetValueSet(copyValueSet) - // If we have any requests to shrink call sequences, do so now. for _, shrinkVerifier := range shrinkVerifiers { _, err = fw.shrinkCallSequence(callSequence, shrinkVerifier) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 09653c6f..1d92acb1 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -198,11 +198,7 @@ func (t *ValueGenerationTracer) getCallFrameReturnValue() TransactionOutputValue // Resolve our method definition if t.currentCallFrame.CodeContractAbi != nil { - if t.currentCallFrame.IsContractCreation() { - method = &t.currentCallFrame.CodeContractAbi.Constructor - } else { - method, _ = t.currentCallFrame.CodeContractAbi.MethodById(t.currentCallFrame.InputData) - } + method, _ = t.currentCallFrame.CodeContractAbi.MethodById(t.currentCallFrame.InputData) } if method != nil { From 93799c0945d1068513bc02d9f0a8963802c78ef0 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:25:49 +0400 Subject: [PATCH 36/55] Move ExperimentalValueGenerationEnabled to TestingConfig --- docs/src/static/medusa.json | 2 +- fuzzing/config/config_defaults.go | 34 +++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/src/static/medusa.json b/docs/src/static/medusa.json index 551ab4e0..4e297f54 100644 --- a/docs/src/static/medusa.json +++ b/docs/src/static/medusa.json @@ -7,7 +7,7 @@ "callSequenceLength": 100, "corpusDirectory": "", "coverageEnabled": true, - "ExperimentalValueGenerationEnabled": true, + "experimentalValueGenerationEnabled": true, "targetContracts": [], "targetContractsBalances": [], "constructorArgs": {}, diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 4ae6568f..30d2f174 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -33,18 +33,17 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { // Create a project configuration projectConfig := &ProjectConfig{ Fuzzing: FuzzingConfig{ - Workers: 10, - WorkerResetLimit: 50, - Timeout: 0, - TestLimit: 0, - ShrinkLimit: 5_000, - CallSequenceLength: 100, - TargetContracts: []string{}, - TargetContractsBalances: []*big.Int{}, - ConstructorArgs: map[string]map[string]any{}, - CorpusDirectory: "", - CoverageEnabled: true, - ExperimentalValueGenerationEnabled: false, + Workers: 10, + WorkerResetLimit: 50, + Timeout: 0, + TestLimit: 0, + ShrinkLimit: 5_000, + CallSequenceLength: 100, + TargetContracts: []string{}, + TargetContractsBalances: []*big.Int{}, + ConstructorArgs: map[string]map[string]any{}, + CorpusDirectory: "", + CoverageEnabled: true, SenderAddresses: []string{ "0x10000", "0x20000", @@ -56,11 +55,12 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { BlockGasLimit: 125_000_000, TransactionGasLimit: 12_500_000, Testing: TestingConfig{ - StopOnFailedTest: true, - StopOnFailedContractMatching: false, - StopOnNoTests: true, - TestAllContracts: false, - TraceAll: false, + StopOnFailedTest: true, + StopOnFailedContractMatching: false, + StopOnNoTests: true, + TestAllContracts: false, + TraceAll: false, + ExperimentalValueGenerationEnabled: false, AssertionTesting: AssertionTestingConfig{ Enabled: true, TestViewMethods: false, From ab6d871d3282c1e91ad64e007eb2e0824e5830f4 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:37:19 +0400 Subject: [PATCH 37/55] Remove type TransactionOutputValues --- .../valuegeneration_tracer.go | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 1d92acb1..5227a642 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -1,11 +1,9 @@ package valuegenerationtracer import ( - "fmt" "github.com/crytic/medusa/chain/types" "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/contracts" - "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -20,13 +18,11 @@ import ( // querying them. const valueGenerationTracerResultsKey = "ValueGenerationTracerResults" -type TransactionOutputValues []any - type ValueGenerationTrace struct { TopLevelCallFrame *utils.CallFrame contractDefinitions contracts.Contracts - transactionOutputValues TransactionOutputValues + transactionOutputValues []any } // TODO: Sanan @@ -189,12 +185,13 @@ func (v *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *u } // getCallFrameReturnValue generates a list of elements describing the return value of the call frame -func (t *ValueGenerationTracer) getCallFrameReturnValue() TransactionOutputValues { +func (t *ValueGenerationTracer) getCallFrameReturnValue() any { // Define some strings that represent our current call frame var method *abi.Method // Define a slice of any to represent return values of the current call frame - var outputValues TransactionOutputValues + //var outputValues TransactionOutputValues + var outputValue any // Resolve our method definition if t.currentCallFrame.CodeContractAbi != nil { @@ -306,7 +303,7 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. //events := make([]EventInputs, 0) // Collect generated event and return values of the current transaction - eventAndReturnValues := make(TransactionOutputValues, 0) + eventAndReturnValues := make([]any, 0) eventAndReturnValues = v.trace.generateEvents(v.trace.TopLevelCallFrame, eventAndReturnValues) v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, eventAndReturnValues) @@ -315,9 +312,8 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. } -func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, events TransactionOutputValues) TransactionOutputValues { - for iter, operation := range currentCallFrame.Operations { - fmt.Printf("Iteration: %d\n", iter) +func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, events []any) []any { + for _, operation := range currentCallFrame.Operations { if childCallFrame, ok := operation.(*utils.CallFrame); ok { // If this is a call frame being entered, generate information recursively. t.generateEvents(childCallFrame, events) @@ -332,7 +328,7 @@ func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, return events } -func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) TransactionOutputValues { +func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) []any { // Try to unpack our event data eventInputs := t.transactionOutputValues event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) From f2019f5e6083876f2f6360c66561bd7d7c0e8b6a Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:38:07 +0400 Subject: [PATCH 38/55] Add comment descriptions to ValueGenerationTracer members --- fuzzing/valuegenerationtracer/valuegeneration_tracer.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 5227a642..c14b1e93 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -19,8 +19,12 @@ import ( const valueGenerationTracerResultsKey = "ValueGenerationTracerResults" type ValueGenerationTrace struct { + // TopLevelCallFrame refers to the root call frame, the first EVM call scope entered when an externally-owned + // address calls upon a contract. TopLevelCallFrame *utils.CallFrame + // contractDefinitions represents the known contract definitions at the time of tracing. This is used to help + // obtain any additional information regarding execution. contractDefinitions contracts.Contracts transactionOutputValues []any } From 072c8cb505c7f6a76fb1675f20e29ae271d5c4a6 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:39:06 +0400 Subject: [PATCH 39/55] Remove stale comments and print statements --- .../valuegeneration_tracer.go | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index c14b1e93..62f3b9ff 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -29,7 +29,6 @@ type ValueGenerationTrace struct { transactionOutputValues []any } -// TODO: Sanan type ValueGenerationTracer struct { // evm refers to the EVM instance last captured. evm *vm.EVM @@ -51,8 +50,6 @@ type ValueGenerationTracer struct { } func NewValueGenerationTracer(contractDefinitions contracts.Contracts) *ValueGenerationTracer { - fmt.Println("Called NewValueGenerationTracer") - // TODO: Sanan tracer := &ValueGenerationTracer{ contractDefinitions: contractDefinitions, } @@ -60,39 +57,28 @@ func NewValueGenerationTracer(contractDefinitions contracts.Contracts) *ValueGen } func (v *ValueGenerationTracer) CaptureTxStart(gasLimit uint64) { - // Sanan: start fresh - //v.callDepth = 0 v.trace = newValueGenerationTrace(v.contractDefinitions) v.currentCallFrame = nil v.onNextCaptureState = nil } func (v *ValueGenerationTracer) CaptureTxEnd(restGas uint64) { - //TODO implement me - //panic("implement me") } func (v *ValueGenerationTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - //TODO implement me v.evm = env v.captureEnteredCallFrame(from, to, input, create, value) - //panic("implement me") } func (v *ValueGenerationTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { - //TODO implement me - //panic("implement me") v.captureExitedCallFrame(output, err) } func (v *ValueGenerationTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - //TODO implement me v.captureEnteredCallFrame(from, to, input, typ == vm.CREATE || typ == vm.CREATE2, value) } func (v *ValueGenerationTracer) CaptureExit(output []byte, gasUsed uint64, err error) { - //TODO implement me - //panic("implement me") v.captureExitedCallFrame(output, err) } @@ -106,7 +92,6 @@ func newValueGenerationTrace(contracts contracts.Contracts) *ValueGenerationTrac // captureEnteredCallFrame is a helper method used when a new call frame is entered to record information about it. func (v *ValueGenerationTracer) captureEnteredCallFrame(fromAddress common.Address, toAddress common.Address, inputData []byte, isContractCreation bool, value *big.Int) { // Create our call frame struct to track data for this call frame we entered. - fmt.Println("Entered captureEnteredCallFrame") callFrameData := &utils.CallFrame{ SenderAddress: fromAddress, ToAddress: toAddress, @@ -268,8 +253,6 @@ func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost } func (v *ValueGenerationTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { - //TODO implement me - //panic("implement me") } func AddTransactionOutputValuesToValueSet(results *types.MessageResults, valueSet *valuegeneration.ValueSet) { @@ -301,11 +284,6 @@ func AddTransactionOutputValuesToValueSet(results *types.MessageResults, valueSe // 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 (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types.MessageResults) { - // Store our tracer results. - //results.Receipt.Logs = v.currentCallFrame.Operations - //var eventLogs []*coreTypes.Log - //events := make([]EventInputs, 0) - // Collect generated event and return values of the current transaction eventAndReturnValues := make([]any, 0) eventAndReturnValues = v.trace.generateEvents(v.trace.TopLevelCallFrame, eventAndReturnValues) @@ -325,7 +303,6 @@ func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, // If an event log was emitted, add a message for it. events = append(events, t.getEventsGenerated(currentCallFrame, eventLog)...) //t.getEventsGenerated(currentCallFrame, eventLog) - fmt.Printf("Value of events: %v\n", events) //eventLogs = append(eventLogs, eventLog) } } From d8cce908a72438e4ef8c54a434a793b4130f6865 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:40:18 +0400 Subject: [PATCH 40/55] Prevent return values from being reset --- .../valuegeneration_tracer.go | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 62f3b9ff..80b7e1fc 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -71,7 +71,7 @@ func (v *ValueGenerationTracer) CaptureStart(env *vm.EVM, from common.Address, t } func (v *ValueGenerationTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { - v.captureExitedCallFrame(output, err) + v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, v.captureExitedCallFrame(output, err)) } func (v *ValueGenerationTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { @@ -79,7 +79,7 @@ func (v *ValueGenerationTracer) CaptureEnter(typ vm.OpCode, from common.Address, } func (v *ValueGenerationTracer) CaptureExit(output []byte, gasUsed uint64, err error) { - v.captureExitedCallFrame(output, err) + v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, v.captureExitedCallFrame(output, err)) } func newValueGenerationTrace(contracts contracts.Contracts) *ValueGenerationTrace { @@ -190,16 +190,17 @@ func (t *ValueGenerationTracer) getCallFrameReturnValue() any { if method != nil { // Unpack our output values and obtain a string to represent them, only if we didn't encounter an error. if t.currentCallFrame.ReturnError == nil { - outputValue, _ := method.Outputs.Unpack(t.currentCallFrame.ReturnData) - outputValues = append(outputValues, outputValue) + outputValue, _ = method.Outputs.Unpack(t.currentCallFrame.ReturnData) + //outputValues = append(outputValues, outputValue) } } - return outputValues + return outputValue } // captureExitedCallFrame is a helper method used when a call frame is exited, to record information about it. -func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) { +func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) any { + // If this was an initial deployment, now that we're exiting, we'll want to record the finally deployed bytecodes. if v.currentCallFrame.ToRuntimeBytecode == nil { // As long as this isn't a failed contract creation, we should be able to fetch "to" byte code on exit. @@ -224,13 +225,16 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) v.currentCallFrame.ReturnData = slices.Clone(output) v.currentCallFrame.ReturnError = err + var returnValue any if v.currentCallFrame.ReturnError == nil { // Grab return data of the call frame - v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, v.getCallFrameReturnValue()) + returnValue = v.getCallFrameReturnValue() } // We're exiting the current frame, so set our current call frame to the parent v.currentCallFrame = v.currentCallFrame.ParentCallFrame + + return returnValue } func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { @@ -288,7 +292,9 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. eventAndReturnValues := make([]any, 0) eventAndReturnValues = v.trace.generateEvents(v.trace.TopLevelCallFrame, eventAndReturnValues) - v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, eventAndReturnValues) + if len(eventAndReturnValues) > 0 { + v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, eventAndReturnValues) + } results.AdditionalResults[valueGenerationTracerResultsKey] = v.trace.transactionOutputValues @@ -311,7 +317,7 @@ func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) []any { // Try to unpack our event data - eventInputs := t.transactionOutputValues + eventInputs := make([]any, 0) event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) if event == nil { From 5e10173b147c80da8b3ff8e88e3178987fe13659 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:42:29 +0400 Subject: [PATCH 41/55] Make adding transaction output values value_set.go's responsibility Considering clean desing, the function responsible for adding transaction output values to value set is moved to value_set.go and turned into a method called Add. --- fuzzing/valuegeneration/value_set.go | 45 +++++++++++++++++++ .../valuegeneration_tracer.go | 25 ----------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/fuzzing/valuegeneration/value_set.go b/fuzzing/valuegeneration/value_set.go index 883aab73..e697a744 100644 --- a/fuzzing/valuegeneration/value_set.go +++ b/fuzzing/valuegeneration/value_set.go @@ -2,6 +2,7 @@ package valuegeneration import ( "encoding/hex" + "github.com/crytic/medusa/chain/types" "hash" "math/big" @@ -172,3 +173,47 @@ func (vs *ValueSet) RemoveBytes(b []byte) { delete(vs.bytes, hashStr) } + +func (vs *ValueSet) Add(results *types.MessageResults) { + valueGenerationTracerResults := results.AdditionalResults["ValueGenerationTracerResults"] + + if transactionOutputValues, ok := valueGenerationTracerResults.([]any); ok { + + for _, eventOrReturnValues := range transactionOutputValues { + //if eventOrReturnSlice, ok := eventOrReturnValues.(TransactionOutputValues); ok { + // for _, eventOrReturnValue := range eventOrReturnSlice { + // switch v := eventOrReturnValue.(type) { + // case *big.Int: + // valueSet.AddInteger(v) + // case common.Address: + // valueSet.AddAddress(v) + // case string: + // valueSet.AddString(v) + // case []byte: + // valueSet.AddBytes(v) + // default: + // continue + // } + // + // } + if eventOrReturnSlice, ok := eventOrReturnValues.([]any); ok { + for _, eventOrReturnValue := range eventOrReturnSlice { + switch v := eventOrReturnValue.(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/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index 80b7e1fc..ba6dcf6d 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -259,31 +259,6 @@ func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost func (v *ValueGenerationTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { } -func AddTransactionOutputValuesToValueSet(results *types.MessageResults, valueSet *valuegeneration.ValueSet) { - valueGenerationTracerResults := results.AdditionalResults["ValueGenerationTracerResults"] - - if transactionOutputValues, ok := valueGenerationTracerResults.(TransactionOutputValues); ok { - - for _, eventOrReturnValue := range transactionOutputValues { - switch v := eventOrReturnValue.(type) { - case *big.Int: - valueSet.AddInteger(v) - case common.Address: - valueSet.AddAddress(v) - case string: - valueSet.AddString(v) - case []byte: - valueSet.AddBytes(v) - default: - continue - - } - } - } - - fmt.Printf("ValueSet after modification: %v\n", valueSet) -} - // 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. From b496c43709ba936bd4bbe6aca5c0683b4db60908 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:43:22 +0400 Subject: [PATCH 42/55] Use Add instead of deprecated AddTransactionOutputValuesToValueSet --- fuzzing/fuzzer_worker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 71c7540e..9ffaa4bb 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -290,7 +290,7 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall // Add event values to copied ValueSet lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] messageResults := lastExecutedSequenceElement.ChainReference.MessageResults() - valuegenerationtracer.AddTransactionOutputValuesToValueSet(messageResults, fw.valueSet) + fw.ValueSet().Add(messageResults) // Loop through each test function, signal our worker tested a call, and collect any requests to shrink // this call sequence. From f9dd9a0736de96178ed319d5abca8e1f8759bd3f Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:44:12 +0400 Subject: [PATCH 43/55] Use fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled --- fuzzing/fuzzer_worker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 9ffaa4bb..93a26137 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -563,7 +563,7 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { // 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.ExperimentalValueGenerationEnabled { + if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled { fw.valueGenerationTracer = valuegenerationtracer.NewValueGenerationTracer(fw.fuzzer.contractDefinitions) initializedChain.AddTracer(fw.valueGenerationTracer, true, false) } From 00e8edb4a19f70682ff8b9f8d3b14b1997bd90b0 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:48:11 +0400 Subject: [PATCH 44/55] Check whether all values are added and if the base value set is preserved --- fuzzing/fuzzer_test.go | 37 ++++++++++++++++++++++++----- fuzzing/fuzzer_test_methods_test.go | 31 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 77ee9ace..15ae89e9 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -60,7 +60,11 @@ func TestFuzzerHooks(t *testing.T) { }) } -func TestExperimentalValueGeneration_ValueSet(t *testing.T) { +// TestExperimentalValueGeneration_EventsReturnValues runs tests to ensure whether interesting values collected +// during EVM execution is added to the base value set (which gets reset to default at each call sequence execution) +// In addition, it makes sure that the base value set is reset to default after the end of each call sequence +// execution +func TestExperimentalValueGeneration_EventsReturnValues(t *testing.T) { filePaths := []string{ "testdata/contracts/assertions/assert_immediate.sol", } @@ -79,14 +83,35 @@ func TestExperimentalValueGeneration_ValueSet(t *testing.T) { config.Fuzzing.Testing.ExperimentalValueGenerationEnabled = true }, method: func(f *fuzzerTestContext) { - baseValueSet := f.fuzzer.baseValueSet - // Start the fuzzer + valueSet := f.fuzzer.baseValueSet + f.fuzzer.Events.WorkerCreated.Subscribe(func(event FuzzerWorkerCreatedEvent) error { + event.Worker.Events.FuzzerWorkerChainSetup.Subscribe(func(event FuzzerWorkerChainSetupEvent) error { + event.Worker.chain.Events.PendingBlockAddedTx.Subscribe(func(event chain.PendingBlockAddedTxEvent) error { + valueGenerationResults := event.Block.MessageResults[event.TransactionIndex-1].AdditionalResults["ValueGenerationTracerResults"] + res := experimentalValuesAddedToBaseValueSet(f, valueGenerationResults) + assert.True(t, res) + // 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) - valueSet := f.fuzzer.baseValueSet + assert.NoError(t, err) - assert.EqualValues(t, valueSet, baseValueSet) }, }) } diff --git a/fuzzing/fuzzer_test_methods_test.go b/fuzzing/fuzzer_test_methods_test.go index a022551b..625ff3f3 100644 --- a/fuzzing/fuzzer_test_methods_test.go +++ b/fuzzing/fuzzer_test_methods_test.go @@ -1,6 +1,8 @@ package fuzzing import ( + "github.com/ethereum/go-ethereum/common" + "math/big" "testing" "github.com/crytic/medusa/compilation" @@ -111,3 +113,32 @@ func expectEventEmitted[T any](f *fuzzerTestContext, eventEmitter *events.EventE assert.Greater(f.t, f.eventCounter[eventType], 0, "Event was not emitted at all") }) } + +// experimentalValuesAddedToBaseValueSet will ensure whether collected EVM values during execution of the current +// transaction is added to the base value set +func experimentalValuesAddedToBaseValueSet(f *fuzzerTestContext, valueGenerationResults any) bool { + var allAdded bool + currentValueSet := f.fuzzer.workers[0].valueSet + if valGenResults, ok := valueGenerationResults.([]any); ok { + for _, eventOrReturnValues := range valGenResults { + if values, ok := eventOrReturnValues.([]any); ok { + for _, value := range values { + switch v := value.(type) { + case *big.Int: + allAdded = currentValueSet.ContainsInteger(v) + case common.Address: + allAdded = currentValueSet.ContainsAddress(v) + case string: + allAdded = currentValueSet.ContainsString(v) + case []byte: + allAdded = currentValueSet.ContainsBytes(v) + default: + // TODO(Sanan): A byte array with just one element is considered as default, fix. + continue + } + } + } + } + } + return allAdded +} From 826aa8ff08217b2be95de914752213fcd7feb149 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:51:28 +0400 Subject: [PATCH 45/55] Reset assert_immediate.sol to default --- .../contracts/assertions/assert_immediate.sol | 76 ------------------- 1 file changed, 76 deletions(-) diff --git a/fuzzing/testdata/contracts/assertions/assert_immediate.sol b/fuzzing/testdata/contracts/assertions/assert_immediate.sol index 29cd1f56..956a54e2 100644 --- a/fuzzing/testdata/contracts/assertions/assert_immediate.sol +++ b/fuzzing/testdata/contracts/assertions/assert_immediate.sol @@ -1,83 +1,7 @@ -pragma solidity ^0.8.0; - -// This contract includes a function that we will call from TestContract. -contract AnotherContract { - // This function doesn't need to do anything specific for this example. - function externalFunction() public pure returns (string memory) { - return "External function called"; - } -} - // This contract ensures the fuzzer can encounter an immediate assertion failure. contract TestContract { - AnotherContract public anotherContract; - - bytes private constant _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - function random(uint seed) internal view returns (uint) { - return uint(keccak256(abi.encodePacked(block.timestamp, block.difficulty, seed))); - } - - function generateRandomString(uint seed) public view returns (string memory) { - bytes memory result = new bytes(6); - uint rand = random(seed); - - for (uint i = 0; i < 6; i++) { - result[i] = _chars[rand % _chars.length]; - rand = rand / _chars.length; - } - - return string(result); - } - - function generateRandomByteArray(uint seed, uint length) public view returns (bytes memory) { - bytes memory result = new bytes(length); - uint rand = random(seed); - - for (uint i = 0; i < length; i++) { - result[i] = _chars[rand % _chars.length]; - rand = rand / _chars.length; - } - - return result; - } - - event ValueReceived(uint indexed value, uint second_val, string test, bytes byteArray); - event ValueNonIndexedReceived(uint firstval, uint secondval, bytes1 myByte); - - function internalFunction() public returns (string memory, string memory, uint, bytes1, bytes memory) { - string memory internalString = generateRandomString(444); - uint randInt = 4+14; - bytes1 randByte = 0x44; - bytes memory randBytes = generateRandomByteArray(555, 11); - return ("Internal function called", internalString, randInt, randByte, randBytes); - } - - // Deploy AnotherContract within the TestContract - constructor() { -// internalFunction(); - anotherContract = new AnotherContract(); - } - - function callingMeFails(uint value) public { - // Call internalFunction() - internalFunction(); - // Call the external function in AnotherContract. - anotherContract.externalFunction(); - - string memory randString = generateRandomString(123); - bytes memory byteArray = generateRandomByteArray(456, 10); - - uint second_val = 2+12; - - bytes1 myByte = 0x51; - - emit ValueReceived(value, second_val, randString, byteArray); - emit ValueNonIndexedReceived(111+111, 444+444, myByte); - // ASSERTION: We always fail when you call this function. assert(false); - } } From 288337eca8a83b34020a213a48f59589616421a0 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 17 Jul 2024 15:54:48 +0400 Subject: [PATCH 46/55] Add a contract to test event and return value collection This contract will emit event/return values of various types to test value generation tracer's ability to collect them. --- fuzzing/fuzzer_test.go | 37 +------- .../event_and_return_value_emission.sol | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+), 36 deletions(-) create mode 100644 fuzzing/testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 15ae89e9..4117b1cb 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -66,7 +66,7 @@ func TestFuzzerHooks(t *testing.T) { // execution func TestExperimentalValueGeneration_EventsReturnValues(t *testing.T) { filePaths := []string{ - "testdata/contracts/assertions/assert_immediate.sol", + "testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol", } for _, filePath := range filePaths { @@ -116,41 +116,6 @@ func TestExperimentalValueGeneration_EventsReturnValues(t *testing.T) { }) } } -func TestGetEmittedEvents_ValueGeneration(t *testing.T) { - filePaths := []string{ - "testdata/contracts/assertions/assert_immediate.sol", - } - - for _, filePath := range filePaths { - runFuzzerTest(t, &fuzzerSolcFileTest{ - filePath: filePath, - configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.TargetContracts = []string{"TestContract"} - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnAssertion = true - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnAllocateTooMuchMemory = true - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnArithmeticUnderflow = true - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnCallUninitializedVariable = true - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnEnumTypeConversionOutOfBounds = true - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnDivideByZero = true - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnIncorrectStorageAccess = true - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnOutOfBoundsArrayAccess = true - //config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnPopEmptyArray = true - config.Fuzzing.Testing.AssertionTesting.Enabled = false - config.Fuzzing.Testing.PropertyTesting.Enabled = false - config.Fuzzing.Testing.OptimizationTesting.Enabled = false - config.Fuzzing.Testing.StopOnNoTests = false - config.Fuzzing.Workers = 1 - }, - method: func(f *fuzzerTestContext) { - // Start the fuzzer - err := f.fuzzer.Start() - assert.NoError(t, err) - // Check for failed assertion tests. - assertFailedTestsExpected(f, true) - }, - }) - } -} // TestAssertionMode runs tests to ensure that assertion testing behaves as expected. func TestAssertionMode(t *testing.T) { 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..e4b41c68 --- /dev/null +++ b/fuzzing/testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol @@ -0,0 +1,88 @@ +pragma solidity ^0.8.0; + +// This contract includes a function that we will call from TestContract. +contract AnotherContract { + // This function doesn't need to do anything specific for this example. + function externalFunction() public pure returns (string memory) { + return "External function called"; + } +} + +// This contract ensures the fuzzer can encounter an immediate assertion failure. +contract TestContract { + AnotherContract public anotherContract; + + bytes private constant _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + function random(uint seed) internal view returns (uint) { + return uint(keccak256(abi.encodePacked(block.timestamp, block.difficulty, seed))); + } + + function generateRandomString(uint seed) public view returns (string memory) { + bytes memory result = new bytes(6); + uint rand = random(seed); + + for (uint i = 0; i < 6; i++) { + result[i] = _chars[rand % _chars.length]; + rand = rand / _chars.length; + } + + return string(result); + } + + function generateRandomByteArray(uint seed, uint length) public view returns (bytes memory) { + bytes memory result = new bytes(length); + uint rand = random(seed); + + for (uint i = 0; i < length; i++) { + result[i] = _chars[rand % _chars.length]; + rand = rand / _chars.length; + } + + return result; + } + + event ValueReceived(uint indexed value, uint second_val, string test, bytes byteArray, string test2); + event ValueNonIndexedReceived(uint firstval, uint secondval, bytes1 myByte); + event NewEvent(string newEventVal1, string newEventVal2); + + function internalFunction() public returns (string memory, string memory, uint, bytes1, bytes memory) { + string memory internalString = generateRandomString(444); + uint randInt = 4+14; + bytes1 randByte = 0x44; + bytes memory randBytes = generateRandomByteArray(555, 11); + + emit NewEvent("neweventval1", "neweventval2"); + return (string(abi.encodePacked("t", "est")), internalString, randInt, randByte, randBytes); + } + + // Deploy AnotherContract within the TestContract + constructor() { +// internalFunction(); + anotherContract = new AnotherContract(); + } + + + function callingMeFails(uint value) public { +// // Call internalFunction() +// internalFunction(); +// // Call the external function in AnotherContract. +// anotherContract.externalFunction(); + + string memory randString = generateRandomString(123); + bytes memory byteArray = generateRandomByteArray(456, 10); + + uint second_val = 2+12; + + bytes1 myByte = 0x51; + + string memory secondString = string(abi.encodePacked("trailof", "bits")); + + emit ValueReceived(value, second_val, randString, byteArray, secondString); + emit ValueNonIndexedReceived(111+111, 444+444, myByte); + + // ASSERTION: We always fail when you call this function. + assert(false); + + } +} From 30f94190f7b9391f83e8625cf2a7d59bfc735ec6 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Mon, 22 Jul 2024 17:34:30 +0400 Subject: [PATCH 47/55] Add to value set inside the unit test --- fuzzing/fuzzer_test.go | 8 ++-- fuzzing/fuzzer_worker.go | 6 ++- fuzzing/valuegeneration/value_set.go | 55 +++++++++------------------- 3 files changed, 26 insertions(+), 43 deletions(-) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 4117b1cb..05b27371 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -87,9 +87,11 @@ func TestExperimentalValueGeneration_EventsReturnValues(t *testing.T) { f.fuzzer.Events.WorkerCreated.Subscribe(func(event FuzzerWorkerCreatedEvent) error { event.Worker.Events.FuzzerWorkerChainSetup.Subscribe(func(event FuzzerWorkerChainSetupEvent) error { event.Worker.chain.Events.PendingBlockAddedTx.Subscribe(func(event chain.PendingBlockAddedTxEvent) error { - valueGenerationResults := event.Block.MessageResults[event.TransactionIndex-1].AdditionalResults["ValueGenerationTracerResults"] - res := experimentalValuesAddedToBaseValueSet(f, valueGenerationResults) - assert.True(t, res) + if valueGenerationResults, ok := event.Block.MessageResults[event.TransactionIndex-1].AdditionalResults["ValueGenerationTracerResults"].([]any); ok { + f.fuzzer.workers[0].valueSet.Add(valueGenerationResults) + res := experimentalValuesAddedToBaseValueSet(f, valueGenerationResults) + assert.True(t, res) + } // 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 diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 93a26137..ea398662 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -289,8 +289,10 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall // Add event values to copied ValueSet lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] - messageResults := lastExecutedSequenceElement.ChainReference.MessageResults() - fw.ValueSet().Add(messageResults) + + if messageResults, ok := lastExecutedSequenceElement.ChainReference.MessageResults().AdditionalResults["ValueGenerationTracerResults"].([]any); ok { + fw.ValueSet().Add(messageResults) + } // Loop through each test function, signal our worker tested a call, and collect any requests to shrink // this call sequence. diff --git a/fuzzing/valuegeneration/value_set.go b/fuzzing/valuegeneration/value_set.go index e697a744..d60f2cf8 100644 --- a/fuzzing/valuegeneration/value_set.go +++ b/fuzzing/valuegeneration/value_set.go @@ -2,7 +2,6 @@ package valuegeneration import ( "encoding/hex" - "github.com/crytic/medusa/chain/types" "hash" "math/big" @@ -174,44 +173,24 @@ func (vs *ValueSet) RemoveBytes(b []byte) { delete(vs.bytes, hashStr) } -func (vs *ValueSet) Add(results *types.MessageResults) { - valueGenerationTracerResults := results.AdditionalResults["ValueGenerationTracerResults"] - - if transactionOutputValues, ok := valueGenerationTracerResults.([]any); ok { - - for _, eventOrReturnValues := range transactionOutputValues { - //if eventOrReturnSlice, ok := eventOrReturnValues.(TransactionOutputValues); ok { - // for _, eventOrReturnValue := range eventOrReturnSlice { - // switch v := eventOrReturnValue.(type) { - // case *big.Int: - // valueSet.AddInteger(v) - // case common.Address: - // valueSet.AddAddress(v) - // case string: - // valueSet.AddString(v) - // case []byte: - // valueSet.AddBytes(v) - // default: - // continue - // } - // - // } - if eventOrReturnSlice, ok := eventOrReturnValues.([]any); ok { - for _, eventOrReturnValue := range eventOrReturnSlice { - switch v := eventOrReturnValue.(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 - } - +func (vs *ValueSet) Add(results []any) { + + for _, eventOrReturnValues := range results { + if eventOrReturnSlice, ok := eventOrReturnValues.([]any); ok { + for _, eventOrReturnValue := range eventOrReturnSlice { + switch v := eventOrReturnValue.(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 } + } } } From 0e1763f3c9d48b02a091eba8dc9f23043f9dd100 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Tue, 23 Jul 2024 19:26:38 +0400 Subject: [PATCH 48/55] Adapt value generation tracer to interface changes --- fuzzing/executiontracer/execution_tracer.go | 1 - fuzzing/fuzzer_worker.go | 2 +- .../valuegeneration_tracer.go | 125 ++++++++++-------- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index db5632a4..d7b3157c 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -6,7 +6,6 @@ import ( "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_worker.go b/fuzzing/fuzzer_worker.go index c5ffb39c..a4b870a2 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -578,7 +578,7 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { // 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, true, false) + initializedChain.AddTracer(fw.valueGenerationTracer.NativeTracer(), true, false) } return nil }) diff --git a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go index ba6dcf6d..0c1e95e0 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -1,6 +1,7 @@ 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" @@ -8,8 +9,10 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" - coreTypes "github.com/ethereum/go-ethereum/core/types" + "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" ) @@ -31,7 +34,7 @@ type ValueGenerationTrace struct { type ValueGenerationTracer struct { // evm refers to the EVM instance last captured. - evm *vm.EVM + evmContext *tracing.VMContext // trace represents the current execution trace captured by this tracer. trace *ValueGenerationTrace @@ -47,39 +50,32 @@ type ValueGenerationTracer struct { // after some state is captured, on the next state capture (e.g. detecting a log instruction, but // using this structure to execute code later once the log is committed). onNextCaptureState []func() + + // 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 +} func NewValueGenerationTracer(contractDefinitions contracts.Contracts) *ValueGenerationTracer { tracer := &ValueGenerationTracer{ contractDefinitions: contractDefinitions, } - return tracer -} -func (v *ValueGenerationTracer) CaptureTxStart(gasLimit uint64) { - v.trace = newValueGenerationTrace(v.contractDefinitions) - v.currentCallFrame = nil - v.onNextCaptureState = nil -} - -func (v *ValueGenerationTracer) CaptureTxEnd(restGas uint64) { -} - -func (v *ValueGenerationTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - v.evm = env - v.captureEnteredCallFrame(from, to, input, create, value) -} - -func (v *ValueGenerationTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { - v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, v.captureExitedCallFrame(output, err)) -} - -func (v *ValueGenerationTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - v.captureEnteredCallFrame(from, to, input, typ == vm.CREATE || typ == vm.CREATE2, value) -} - -func (v *ValueGenerationTracer) CaptureExit(output []byte, gasUsed uint64, err error) { - v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, v.captureExitedCallFrame(output, err)) + 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 } func newValueGenerationTrace(contracts contracts.Contracts) *ValueGenerationTrace { @@ -205,7 +201,7 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) if v.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 !v.currentCallFrame.IsContractCreation() || err == nil { - v.currentCallFrame.ToRuntimeBytecode = v.evm.StateDB.GetCode(v.currentCallFrame.ToAddress) + v.currentCallFrame.ToRuntimeBytecode = v.evmContext.StateDB.GetCode(v.currentCallFrame.ToAddress) } } if v.currentCallFrame.CodeRuntimeBytecode == nil { @@ -214,7 +210,7 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) if v.currentCallFrame.CodeAddress == v.currentCallFrame.ToAddress { v.currentCallFrame.CodeRuntimeBytecode = v.currentCallFrame.ToRuntimeBytecode } else { - v.currentCallFrame.CodeRuntimeBytecode = v.evm.StateDB.GetCode(v.currentCallFrame.CodeAddress) + v.currentCallFrame.CodeRuntimeBytecode = v.evmContext.StateDB.GetCode(v.currentCallFrame.CodeAddress) } } @@ -237,28 +233,6 @@ func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) return returnValue } -func (v *ValueGenerationTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { - // TODO: look for RET opcode (for now try getting them from currentCallFrame.ReturnData) - // Execute all "on next capture state" events and clear them. - for _, eventHandler := range v.onNextCaptureState { - eventHandler() - } - v.onNextCaptureState = nil - - // If a log operation occurred, add a deferred operation to capture it. - if op == vm.LOG0 || op == vm.LOG1 || op == vm.LOG2 || op == vm.LOG3 || op == vm.LOG4 { - v.onNextCaptureState = append(v.onNextCaptureState, func() { - logs := v.evm.StateDB.(*state.StateDB).Logs() - if len(logs) > 0 { - v.currentCallFrame.Operations = append(v.currentCallFrame.Operations, logs[len(logs)-1]) - } - }) - } -} - -func (v *ValueGenerationTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { -} - // 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. @@ -275,12 +249,55 @@ func (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types. } +// 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.contractDefinitions) + t.currentCallFrame = nil + t.onNextCaptureState = nil + // Store our evm reference + t.evmContext = vm +} + +func (t *ValueGenerationTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + t.captureEnteredCallFrame(from, to, input, typ == byte(vm.CREATE) || typ == byte(vm.CREATE2), value) +} + +func (t *ValueGenerationTracer) OnTxEnd(receipt *coretypes.Receipt, err error) { + +} + +func (t *ValueGenerationTracer) OnExit(depth int, output []byte, used uint64, err error, reverted bool) { + t.trace.transactionOutputValues = append(t.trace.transactionOutputValues, t.captureExitedCallFrame(output, err)) +} + +func (t *ValueGenerationTracer) OnOpcode(pc uint64, op byte, gas uint64, cost uint64, scope tracing.OpContext, data []byte, depth int, err error) { + + // TODO: look for RET opcode (for now try getting them from currentCallFrame.ReturnData) + // Execute all "on next capture state" events and clear them. + for _, eventHandler := range t.onNextCaptureState { + eventHandler() + } + t.onNextCaptureState = nil + +} + +func (t *ValueGenerationTracer) OnLog(log *coretypes.Log) { + + // If a log operation occurred, add a deferred operation to capture it. + t.onNextCaptureState = append(t.onNextCaptureState, func() { + logs := t.evmContext.StateDB.(*state.StateDB).Logs() + if len(logs) > 0 { + t.currentCallFrame.Operations = append(t.currentCallFrame.Operations, logs[len(logs)-1]) + } + }) +} + func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, events []any) []any { for _, operation := range currentCallFrame.Operations { if childCallFrame, ok := operation.(*utils.CallFrame); ok { // If this is a call frame being entered, generate information recursively. t.generateEvents(childCallFrame, events) - } else if eventLog, ok := operation.(*coreTypes.Log); ok { + } else if eventLog, ok := operation.(*coretypes.Log); ok { // If an event log was emitted, add a message for it. events = append(events, t.getEventsGenerated(currentCallFrame, eventLog)...) //t.getEventsGenerated(currentCallFrame, eventLog) @@ -290,7 +307,7 @@ func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, return events } -func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coreTypes.Log) []any { +func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coretypes.Log) []any { // Try to unpack our event data eventInputs := make([]any, 0) event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) From 8ac669535bf34652bb55df3aefc36d4af026a1cd Mon Sep 17 00:00:00 2001 From: s4nsec Date: Tue, 30 Jul 2024 18:24:14 +0400 Subject: [PATCH 49/55] Add new mutation strategy - callSeqGenFuncDuplicateAtRandom The function duplicates a call sequence element at index N and places it at index N+1 if N is not equal to one less the length of the sequence. Otherwise, the duplicate is placed at index N-1. --- fuzzing/fuzzer_worker_sequence_generator.go | 26 +++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index edc3b224..2e5892d6 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -2,12 +2,11 @@ package fuzzing import ( "fmt" - "math/big" - "github.com/crytic/medusa/fuzzing/calls" "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 +163,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, @@ -368,6 +374,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. From 2851e969844a7bc5075b9ae2cbba82511ca77566 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Tue, 30 Jul 2024 18:07:30 -0400 Subject: [PATCH 50/55] code cleanup --- .../executiontracer}/call_frame.go | 2 +- fuzzing/executiontracer/execution_trace.go | 13 +- fuzzing/executiontracer/execution_tracer.go | 8 +- fuzzing/fuzzer_test.go | 116 +++---- fuzzing/fuzzer_worker.go | 24 +- fuzzing/valuegeneration/value_set.go | 35 +- fuzzing/valuegenerationtracer/call_frame.go | 67 ++++ .../valuegeneration_tracer.go | 328 ++++++------------ 8 files changed, 281 insertions(+), 312 deletions(-) rename {utils => fuzzing/executiontracer}/call_frame.go (99%) create mode 100644 fuzzing/valuegenerationtracer/call_frame.go diff --git a/utils/call_frame.go b/fuzzing/executiontracer/call_frame.go similarity index 99% rename from utils/call_frame.go rename to fuzzing/executiontracer/call_frame.go index 33b72ea3..560a78a6 100644 --- a/utils/call_frame.go +++ b/fuzzing/executiontracer/call_frame.go @@ -1,4 +1,4 @@ -package utils +package executiontracer import ( "github.com/ethereum/go-ethereum/accounts/abi" diff --git a/fuzzing/executiontracer/execution_trace.go b/fuzzing/executiontracer/execution_trace.go index b895e47b..76953094 100644 --- a/fuzzing/executiontracer/execution_trace.go +++ b/fuzzing/executiontracer/execution_trace.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/crytic/medusa/utils" "regexp" "strings" @@ -24,7 +23,7 @@ import ( type ExecutionTrace struct { // TopLevelCallFrame refers to the root call frame, the first EVM call scope entered when an externally-owned // address calls upon a contract. - TopLevelCallFrame *utils.CallFrame + TopLevelCallFrame *CallFrame // contractDefinitions represents the known contract definitions at the time of tracing. This is used to help // obtain any additional information regarding execution. @@ -43,7 +42,7 @@ func newExecutionTrace(contracts contracts.Contracts) *ExecutionTrace { // This list of elements will hold information about what kind of call it is, wei sent, what method is called, and more. // Additionally, the list may also hold formatting options for console output. This function also returns a non-empty // string in case this call frame represents a call to the console.log precompile contract. -func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *utils.CallFrame) ([]any, string) { +func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) ([]any, string) { // Create list of elements and console log string elements := make([]any, 0) var consoleLogString string @@ -166,7 +165,7 @@ func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *utils.CallFra // generateCallFrameExitElements generates a list of elements describing the return data of the call frame (e.g. // traditional return data, assertion failure, revert data, etc.). Additionally, the list may also hold formatting options for console output. -func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *utils.CallFrame) []any { +func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *CallFrame) []any { // Create list of elements elements := make([]any, 0) @@ -252,7 +251,7 @@ func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *utils.CallFram // generateEventEmittedElements generates a list of elements used to express an event emission. It contains information about an // event log such as the topics and the event data. Additionally, the list may also hold formatting options for console output. -func (t *ExecutionTrace) generateEventEmittedElements(callFrame *utils.CallFrame, eventLog *coreTypes.Log) []any { +func (t *ExecutionTrace) generateEventEmittedElements(callFrame *CallFrame, eventLog *coreTypes.Log) []any { // Create list of elements elements := make([]any, 0) @@ -303,7 +302,7 @@ func (t *ExecutionTrace) generateEventEmittedElements(callFrame *utils.CallFrame // generateElementsAndLogsForCallFrame generates a list of elements and logs for a given call frame and its children. // The list of elements may also hold formatting options for console output. The list of logs represent calls to the // console.log precompile contract. -func (t *ExecutionTrace) generateElementsAndLogsForCallFrame(currentDepth int, callFrame *utils.CallFrame) ([]any, []any) { +func (t *ExecutionTrace) generateElementsAndLogsForCallFrame(currentDepth int, callFrame *CallFrame) ([]any, []any) { // Create list of elements and logs elements := make([]any, 0) consoleLogs := make([]any, 0) @@ -336,7 +335,7 @@ func (t *ExecutionTrace) generateElementsAndLogsForCallFrame(currentDepth int, c // Loop for each operation performed in the call frame, to provide a chronological history of operations in the // frame. for _, operation := range callFrame.Operations { - if childCallFrame, ok := operation.(*utils.CallFrame); ok { + if childCallFrame, ok := operation.(*CallFrame); ok { // If this is a call frame being entered, generate information recursively. childOutputLines, childConsoleLogStrings := t.generateElementsAndLogsForCallFrame(currentDepth+1, childCallFrame) elements = append(elements, childOutputLines...) diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index d7b3157c..7f491fa3 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -51,7 +51,7 @@ type ExecutionTracer struct { traceMap map[common.Hash]*ExecutionTrace // currentCallFrame references the current call frame being traced. - currentCallFrame *utils.CallFrame + currentCallFrame *CallFrame // contractDefinitions represents the contract definitions to match for execution traces. contractDefinitions contracts.Contracts @@ -125,7 +125,7 @@ func (t *ExecutionTracer) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transac // resolveCallFrameConstructorArgs resolves previously unresolved constructor argument ABI data from the call data, if // the call frame provided represents a contract deployment. -func (t *ExecutionTracer) resolveCallFrameConstructorArgs(callFrame *utils.CallFrame, contract *contracts.Contract) { +func (t *ExecutionTracer) resolveCallFrameConstructorArgs(callFrame *CallFrame, contract *contracts.Contract) { // If this is a contract creation and the constructor ABI argument data has not yet been resolved, do so now. if callFrame.ConstructorArgsData == nil && callFrame.IsContractCreation() { // We simply slice the compiled bytecode leading the input data off, and we are left with the constructor @@ -139,7 +139,7 @@ func (t *ExecutionTracer) resolveCallFrameConstructorArgs(callFrame *utils.CallF // resolveCallFrameContractDefinitions resolves previously unresolved contract definitions for the To and Code addresses // used within the provided call frame. -func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *utils.CallFrame) { +func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *CallFrame) { // Try to resolve contract definitions for "to" address if callFrame.ToContractAbi == nil { // Try to resolve definitions from cheat code contracts @@ -186,7 +186,7 @@ func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *utils.C // captureEnteredCallFrame is a helper method used when a new call frame is entered to record information about it. func (t *ExecutionTracer) captureEnteredCallFrame(fromAddress common.Address, 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 := &utils.CallFrame{ + callFrameData := &CallFrame{ SenderAddress: fromAddress, ToAddress: toAddress, // Note: Set temporarily, overwritten if code executes (in OnOpcode) and the contract's address is overridden by delegatecall. ToContractName: "", diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 7f7cfc5b..8ddeeee2 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -62,65 +62,6 @@ func TestFuzzerHooks(t *testing.T) { }) } -// TestExperimentalValueGeneration_EventsReturnValues runs tests to ensure whether interesting values collected -// during EVM execution is added to the base value set (which gets reset to default at each call sequence execution) -// In addition, it makes sure that the base value set is reset to default after the end of each call sequence -// execution -func TestExperimentalValueGeneration_EventsReturnValues(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.AssertionTesting.Enabled = false - config.Fuzzing.Testing.PropertyTesting.Enabled = false - config.Fuzzing.Testing.OptimizationTesting.Enabled = false - config.Fuzzing.Testing.StopOnNoTests = false - config.Fuzzing.Workers = 1 - config.Fuzzing.Testing.ExperimentalValueGenerationEnabled = true - }, - method: func(f *fuzzerTestContext) { - valueSet := f.fuzzer.baseValueSet - f.fuzzer.Events.WorkerCreated.Subscribe(func(event FuzzerWorkerCreatedEvent) error { - 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) - res := experimentalValuesAddedToBaseValueSet(f, valueGenerationResults) - assert.True(t, res) - } - // 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) - - }, - }) - } -} - // TestAssertionMode runs tests to ensure that assertion testing behaves as expected. func TestAssertionMode(t *testing.T) { filePaths := []string{ @@ -995,3 +936,60 @@ 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.AssertionTesting.Enabled = false + 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 + f.fuzzer.Events.WorkerCreated.Subscribe(func(event FuzzerWorkerCreatedEvent) error { + 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) + res := experimentalValuesAddedToBaseValueSet(f, valueGenerationResults) + assert.True(t, res) + } + // 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 a4b870a2..839611fd 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -265,16 +265,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 ValueSet - originalValueSet := fw.ValueSet().Clone() - + // 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) } - fw.valueSet = originalValueSet + // 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. @@ -303,11 +308,14 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall return true, err } - // Add event values to copied ValueSet - lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] + // 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) + } - if messageResults, ok := lastExecutedSequenceElement.ChainReference.MessageResults().AdditionalResults["ValueGenerationTracerResults"].([]any); ok { - fw.ValueSet().Add(messageResults) } // Loop through each test function, signal our worker tested a call, and collect any requests to shrink diff --git a/fuzzing/valuegeneration/value_set.go b/fuzzing/valuegeneration/value_set.go index d60f2cf8..3b3cadf2 100644 --- a/fuzzing/valuegeneration/value_set.go +++ b/fuzzing/valuegeneration/value_set.go @@ -173,26 +173,21 @@ func (vs *ValueSet) RemoveBytes(b []byte) { delete(vs.bytes, hashStr) } -func (vs *ValueSet) Add(results []any) { - - for _, eventOrReturnValues := range results { - if eventOrReturnSlice, ok := eventOrReturnValues.([]any); ok { - for _, eventOrReturnValue := range eventOrReturnSlice { - switch v := eventOrReturnValue.(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 - } - - } +// 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 index 0c1e95e0..b1ab50b5 100644 --- a/fuzzing/valuegenerationtracer/valuegeneration_tracer.go +++ b/fuzzing/valuegenerationtracer/valuegeneration_tracer.go @@ -5,10 +5,7 @@ import ( "github.com/crytic/medusa/chain/types" "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/contracts" - "github.com/crytic/medusa/utils" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" coretypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -21,17 +18,15 @@ import ( // 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 { - // TopLevelCallFrame refers to the root call frame, the first EVM call scope entered when an externally-owned - // address calls upon a contract. - TopLevelCallFrame *utils.CallFrame - - // contractDefinitions represents the known contract definitions at the time of tracing. This is used to help - // obtain any additional information regarding execution. - contractDefinitions contracts.Contracts + // 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 @@ -40,17 +35,11 @@ type ValueGenerationTracer struct { trace *ValueGenerationTrace // currentCallFrame references the current call frame being traced. - currentCallFrame *utils.CallFrame + currentCallFrame *CallFrame // contractDefinitions represents the contract definitions to match for execution traces. contractDefinitions contracts.Contracts - // onNextCaptureState refers to methods which should be executed the next time CaptureState executes. - // CaptureState is called prior to execution of an instruction. This allows actions to be performed - // after some state is captured, on the next state capture (e.g. detecting a log instruction, but - // using this structure to execute code later once the log is committed). - onNextCaptureState []func() - // nativeTracer is the underlying tracer used to capture EVM execution. nativeTracer *chain.TestChainTracer } @@ -59,6 +48,8 @@ type ValueGenerationTracer struct { 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, @@ -78,36 +69,76 @@ func NewValueGenerationTracer(contractDefinitions contracts.Contracts) *ValueGen return tracer } -func newValueGenerationTrace(contracts contracts.Contracts) *ValueGenerationTrace { +// newValueGenerationTrace creates a new ValueGenerationTrace and returns it +func newValueGenerationTrace() *ValueGenerationTrace { return &ValueGenerationTrace{ - TopLevelCallFrame: nil, - contractDefinitions: contracts, + 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 } -// captureEnteredCallFrame is a helper method used when a new call frame is entered to record information about it. -func (v *ValueGenerationTracer) captureEnteredCallFrame(fromAddress common.Address, toAddress common.Address, inputData []byte, isContractCreation bool, value *big.Int) { +// 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 := &utils.CallFrame{ - SenderAddress: fromAddress, + callFrameData := &CallFrame{ ToAddress: toAddress, - ToContractName: "", ToContractAbi: nil, ToInitBytecode: nil, ToRuntimeBytecode: nil, CodeAddress: toAddress, // Note: Set temporarily, overwritten if code executes (in CaptureState). - CodeContractName: "", CodeContractAbi: nil, CodeRuntimeBytecode: nil, - Operations: make([]any, 0), - SelfDestructed: false, + Logs: make([]*coretypes.Log, 0), InputData: slices.Clone(inputData), - ConstructorArgsData: nil, ReturnData: nil, ExecutedCode: false, - CallValue: value, - ReturnError: nil, - ParentCallFrame: v.currentCallFrame, + ParentCallFrame: t.currentCallFrame, } // If this is a contract creation, set the init bytecode for this call frame to the input data. @@ -115,44 +146,70 @@ func (v *ValueGenerationTracer) captureEnteredCallFrame(fromAddress common.Addre callFrameData.ToInitBytecode = inputData } - // Set our current call frame in our trace - if v.trace.TopLevelCallFrame == nil { - v.trace.TopLevelCallFrame = callFrameData - } else { - v.currentCallFrame.Operations = append(v.currentCallFrame.Operations, callFrameData) - } - v.currentCallFrame = callFrameData + // Update our current call frame + t.currentCallFrame = callFrameData } -// resolveConstructorArgs resolves previously unresolved constructor argument ABI data from the call data, if -// the call frame provided represents a contract deployment. -func (v *ValueGenerationTracer) resolveCallFrameConstructorArgs(callFrame *utils.CallFrame, contract *contracts.Contract) { - // If this is a contract creation and the constructor ABI argument data has not yet been resolved, do so now. - if callFrame.ConstructorArgsData == nil && callFrame.IsContractCreation() { - // We simply slice the compiled bytecode leading the input data off, and we are left with the constructor - // arguments ABI data. - compiledInitBytecode := contract.CompiledContract().InitBytecode - if len(compiledInitBytecode) <= len(callFrame.InputData) { - callFrame.ConstructorArgsData = callFrame.InputData[len(compiledInitBytecode):] +// 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 (v *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *utils.CallFrame) { +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 := v.contractDefinitions.MatchBytecode(callFrame.ToInitBytecode, callFrame.ToRuntimeBytecode) + toContract := t.contractDefinitions.MatchBytecode(callFrame.ToInitBytecode, callFrame.ToRuntimeBytecode) if toContract != nil { - callFrame.ToContractName = toContract.Name() callFrame.ToContractAbi = &toContract.CompiledContract().Abi - v.resolveCallFrameConstructorArgs(callFrame, toContract) // If this is a contract creation, set the code address to the address of the contract we just deployed. if callFrame.IsContractCreation() { - callFrame.CodeContractName = toContract.Name() callFrame.CodeContractAbi = &toContract.CompiledContract().Abi } } @@ -160,173 +217,18 @@ func (v *ValueGenerationTracer) resolveCallFrameContractDefinitions(callFrame *u // Try to resolve contract definitions for "code" address if callFrame.CodeContractAbi == nil { - codeContract := v.contractDefinitions.MatchBytecode(nil, callFrame.CodeRuntimeBytecode) + codeContract := t.contractDefinitions.MatchBytecode(nil, callFrame.CodeRuntimeBytecode) if codeContract != nil { - callFrame.CodeContractName = codeContract.Name() callFrame.CodeContractAbi = &codeContract.CompiledContract().Abi + callFrame.ExecutedCode = true } } } -// getCallFrameReturnValue generates a list of elements describing the return value of the call frame -func (t *ValueGenerationTracer) getCallFrameReturnValue() any { - // Define some strings that represent our current call frame - var method *abi.Method - - // Define a slice of any to represent return values of the current call frame - //var outputValues TransactionOutputValues - var outputValue any - - // Resolve our method definition - if t.currentCallFrame.CodeContractAbi != nil { - method, _ = t.currentCallFrame.CodeContractAbi.MethodById(t.currentCallFrame.InputData) - } - - if method != nil { - // Unpack our output values and obtain a string to represent them, only if we didn't encounter an error. - if t.currentCallFrame.ReturnError == nil { - outputValue, _ = method.Outputs.Unpack(t.currentCallFrame.ReturnData) - //outputValues = append(outputValues, outputValue) - } - } - - return outputValue -} - -// captureExitedCallFrame is a helper method used when a call frame is exited, to record information about it. -func (v *ValueGenerationTracer) captureExitedCallFrame(output []byte, err error) any { - - // If this was an initial deployment, now that we're exiting, we'll want to record the finally deployed bytecodes. - if v.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 !v.currentCallFrame.IsContractCreation() || err == nil { - v.currentCallFrame.ToRuntimeBytecode = v.evmContext.StateDB.GetCode(v.currentCallFrame.ToAddress) - } - } - if v.currentCallFrame.CodeRuntimeBytecode == nil { - // Optimization: If the "to" and "code" addresses match, we can simply set our "code" already fetched "to" - // runtime bytecode. - if v.currentCallFrame.CodeAddress == v.currentCallFrame.ToAddress { - v.currentCallFrame.CodeRuntimeBytecode = v.currentCallFrame.ToRuntimeBytecode - } else { - v.currentCallFrame.CodeRuntimeBytecode = v.evmContext.StateDB.GetCode(v.currentCallFrame.CodeAddress) - } - } - - // Resolve our contract definitions on the call frame data, if they have not been. - v.resolveCallFrameContractDefinitions(v.currentCallFrame) - - // Set our information for this call frame - v.currentCallFrame.ReturnData = slices.Clone(output) - v.currentCallFrame.ReturnError = err - - var returnValue any - if v.currentCallFrame.ReturnError == nil { - // Grab return data of the call frame - returnValue = v.getCallFrameReturnValue() - } - - // We're exiting the current frame, so set our current call frame to the parent - v.currentCallFrame = v.currentCallFrame.ParentCallFrame - - return returnValue -} - // 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 (v *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types.MessageResults) { - // Collect generated event and return values of the current transaction - eventAndReturnValues := make([]any, 0) - eventAndReturnValues = v.trace.generateEvents(v.trace.TopLevelCallFrame, eventAndReturnValues) - - if len(eventAndReturnValues) > 0 { - v.trace.transactionOutputValues = append(v.trace.transactionOutputValues, eventAndReturnValues) - } - - results.AdditionalResults[valueGenerationTracerResultsKey] = v.trace.transactionOutputValues - -} - -// 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.contractDefinitions) - t.currentCallFrame = nil - t.onNextCaptureState = nil - // Store our evm reference - t.evmContext = vm -} - -func (t *ValueGenerationTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - t.captureEnteredCallFrame(from, to, input, typ == byte(vm.CREATE) || typ == byte(vm.CREATE2), value) -} - -func (t *ValueGenerationTracer) OnTxEnd(receipt *coretypes.Receipt, err error) { - -} - -func (t *ValueGenerationTracer) OnExit(depth int, output []byte, used uint64, err error, reverted bool) { - t.trace.transactionOutputValues = append(t.trace.transactionOutputValues, t.captureExitedCallFrame(output, err)) -} - -func (t *ValueGenerationTracer) OnOpcode(pc uint64, op byte, gas uint64, cost uint64, scope tracing.OpContext, data []byte, depth int, err error) { - - // TODO: look for RET opcode (for now try getting them from currentCallFrame.ReturnData) - // Execute all "on next capture state" events and clear them. - for _, eventHandler := range t.onNextCaptureState { - eventHandler() - } - t.onNextCaptureState = nil - -} - -func (t *ValueGenerationTracer) OnLog(log *coretypes.Log) { - - // If a log operation occurred, add a deferred operation to capture it. - t.onNextCaptureState = append(t.onNextCaptureState, func() { - logs := t.evmContext.StateDB.(*state.StateDB).Logs() - if len(logs) > 0 { - t.currentCallFrame.Operations = append(t.currentCallFrame.Operations, logs[len(logs)-1]) - } - }) -} - -func (t *ValueGenerationTrace) generateEvents(currentCallFrame *utils.CallFrame, events []any) []any { - for _, operation := range currentCallFrame.Operations { - if childCallFrame, ok := operation.(*utils.CallFrame); ok { - // If this is a call frame being entered, generate information recursively. - t.generateEvents(childCallFrame, events) - } else if eventLog, ok := operation.(*coretypes.Log); ok { - // If an event log was emitted, add a message for it. - events = append(events, t.getEventsGenerated(currentCallFrame, eventLog)...) - //t.getEventsGenerated(currentCallFrame, eventLog) - //eventLogs = append(eventLogs, eventLog) - } - } - return events -} - -func (t *ValueGenerationTrace) getEventsGenerated(callFrame *utils.CallFrame, eventLog *coretypes.Log) []any { - // Try to unpack our event data - eventInputs := make([]any, 0) - event, eventInputValues := abiutils.UnpackEventAndValues(callFrame.CodeContractAbi, eventLog) - - if event == nil { - // If we couldn't resolve the event from our immediate contract ABI, it may come from a library. - for _, contract := range t.contractDefinitions { - event, eventInputValues = abiutils.UnpackEventAndValues(&contract.CompiledContract().Abi, eventLog) - if event != nil { - break - } - } - } - - if event != nil { - for _, value := range eventInputValues { - eventInputs = append(eventInputs, value) - } - } - - return eventInputs +func (t *ValueGenerationTracer) CaptureTxEndSetAdditionalResults(results *types.MessageResults) { + results.AdditionalResults[valueGenerationTracerResultsKey] = t.trace.transactionOutputValues } From 14f677ba1259fae7a729c954e6f976e3de2db980 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Tue, 30 Jul 2024 18:34:58 -0400 Subject: [PATCH 51/55] fix test contract --- fuzzing/fuzzer_test.go | 1 - .../event_and_return_value_emission.sol | 89 +++++-------------- 2 files changed, 24 insertions(+), 66 deletions(-) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 8ddeeee2..395d9d13 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -951,7 +951,6 @@ func TestExperimentalValueGeneration(t *testing.T) { configUpdates: func(config *config.ProjectConfig) { config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.TestLimit = 500 - config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false config.Fuzzing.Workers = 1 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 index e4b41c68..8405cd96 100644 --- a/fuzzing/testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol +++ b/fuzzing/testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol @@ -1,85 +1,44 @@ pragma solidity ^0.8.0; -// This contract includes a function that we will call from TestContract. contract AnotherContract { - // This function doesn't need to do anything specific for this example. - function externalFunction() public pure returns (string memory) { - return "External function called"; + // 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); } } -// This contract ensures the fuzzer can encounter an immediate assertion failure. contract TestContract { AnotherContract public anotherContract; - bytes private constant _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - function random(uint seed) internal view returns (uint) { - return uint(keccak256(abi.encodePacked(block.timestamp, block.difficulty, seed))); - } - - function generateRandomString(uint seed) public view returns (string memory) { - bytes memory result = new bytes(6); - uint rand = random(seed); - - for (uint i = 0; i < 6; i++) { - result[i] = _chars[rand % _chars.length]; - rand = rand / _chars.length; - } - - return string(result); - } - - function generateRandomByteArray(uint seed, uint length) public view returns (bytes memory) { - bytes memory result = new bytes(length); - uint rand = random(seed); - - for (uint i = 0; i < length; i++) { - result[i] = _chars[rand % _chars.length]; - rand = rand / _chars.length; - } - - return result; - } - - event ValueReceived(uint indexed value, uint second_val, string test, bytes byteArray, string test2); - event ValueNonIndexedReceived(uint firstval, uint secondval, bytes1 myByte); - event NewEvent(string newEventVal1, string newEventVal2); - - function internalFunction() public returns (string memory, string memory, uint, bytes1, bytes memory) { - string memory internalString = generateRandomString(444); - uint randInt = 4+14; - bytes1 randByte = 0x44; - bytes memory randBytes = generateRandomByteArray(555, 11); - - emit NewEvent("neweventval1", "neweventval2"); - return (string(abi.encodePacked("t", "est")), internalString, randInt, randByte, randBytes); - } + event EventValues(uint indexed myUint, int myInt, string myStr, address myAddr, bytes myBytes, bytes4 fixedBytes); // Deploy AnotherContract within the TestContract constructor() { -// internalFunction(); 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"; - function callingMeFails(uint value) public { -// // Call internalFunction() -// internalFunction(); -// // Call the external function in AnotherContract. -// anotherContract.externalFunction(); - - string memory randString = generateRandomString(123); - bytes memory byteArray = generateRandomByteArray(456, 10); - - uint second_val = 2+12; - - bytes1 myByte = 0x51; - - string memory secondString = string(abi.encodePacked("trailof", "bits")); + // Call an external contract + anotherContract.testAnotherFunction(x); - emit ValueReceived(value, second_val, randString, byteArray, secondString); - emit ValueNonIndexedReceived(111+111, 444+444, myByte); + // 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); From 64e3ca2de6aae2b3a0b2a33751ee64389a017b6a Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Wed, 31 Jul 2024 10:38:06 -0400 Subject: [PATCH 52/55] wipe value set before test --- fuzzing/fuzzer_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 395d9d13..c2eade67 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -959,6 +959,8 @@ func TestExperimentalValueGeneration(t *testing.T) { method: func(f *fuzzerTestContext) { valueSet := f.fuzzer.baseValueSet 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 { From e94a66840c8020020f577749edd6ef9a47cda634 Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 31 Jul 2024 19:52:56 +0400 Subject: [PATCH 53/55] Add expected values to test whether those values are added to the value set --- fuzzing/fuzzer_test.go | 26 ++++++++++++++++++++++-- fuzzing/fuzzer_test_methods_test.go | 31 ----------------------------- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index c2eade67..48e3f2d3 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -958,6 +958,11 @@ func TestExperimentalValueGeneration(t *testing.T) { }, 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() @@ -965,8 +970,25 @@ func TestExperimentalValueGeneration(t *testing.T) { 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) - res := experimentalValuesAddedToBaseValueSet(f, valueGenerationResults) - assert.True(t, res) + 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 diff --git a/fuzzing/fuzzer_test_methods_test.go b/fuzzing/fuzzer_test_methods_test.go index 625ff3f3..a022551b 100644 --- a/fuzzing/fuzzer_test_methods_test.go +++ b/fuzzing/fuzzer_test_methods_test.go @@ -1,8 +1,6 @@ package fuzzing import ( - "github.com/ethereum/go-ethereum/common" - "math/big" "testing" "github.com/crytic/medusa/compilation" @@ -113,32 +111,3 @@ func expectEventEmitted[T any](f *fuzzerTestContext, eventEmitter *events.EventE assert.Greater(f.t, f.eventCounter[eventType], 0, "Event was not emitted at all") }) } - -// experimentalValuesAddedToBaseValueSet will ensure whether collected EVM values during execution of the current -// transaction is added to the base value set -func experimentalValuesAddedToBaseValueSet(f *fuzzerTestContext, valueGenerationResults any) bool { - var allAdded bool - currentValueSet := f.fuzzer.workers[0].valueSet - if valGenResults, ok := valueGenerationResults.([]any); ok { - for _, eventOrReturnValues := range valGenResults { - if values, ok := eventOrReturnValues.([]any); ok { - for _, value := range values { - switch v := value.(type) { - case *big.Int: - allAdded = currentValueSet.ContainsInteger(v) - case common.Address: - allAdded = currentValueSet.ContainsAddress(v) - case string: - allAdded = currentValueSet.ContainsString(v) - case []byte: - allAdded = currentValueSet.ContainsBytes(v) - default: - // TODO(Sanan): A byte array with just one element is considered as default, fix. - continue - } - } - } - } - } - return allAdded -} From f12eb1ff35cc6cf4eb700307449ffdb973b224ac Mon Sep 17 00:00:00 2001 From: anishnaik Date: Thu, 1 Aug 2024 17:59:35 -0400 Subject: [PATCH 54/55] Enable JSON coverage reports for valuegeneration tracer benchmarking (#426) * initial commit * fix: weight methods correctly to avoid skipping some * fix commenting * add debugging scripts * zero clue if i optimized anything at all... * upload artifact on every PR * fix: log number of workers shrinking (#8) * fix: log number of workers shrinking * report total # failed sequences/ total sequences tested * pushing json coverage report --------- Co-authored-by: alpharush <0xalpharush@protonmail.com> --- .github/workflows/ci.yml | 1 - DEV.md | 104 ++++++++++++++++++++ fuzzing/config/config.go | 8 ++ fuzzing/config/config_defaults.go | 2 + fuzzing/config/gen_fuzzing_config.go | 12 +++ fuzzing/coverage/report_generation.go | 97 ++++++++++++++++-- fuzzing/coverage/report_template.gojson | 15 +++ fuzzing/fuzzer.go | 16 ++- fuzzing/fuzzer_metrics.go | 12 +++ fuzzing/fuzzer_worker.go | 13 +-- fuzzing/fuzzer_worker_sequence_generator.go | 26 +++-- fuzzing/test_case_assertion_provider.go | 2 + fuzzing/test_case_property_provider.go | 1 + go.mod | 4 + go.sum | 8 ++ scripts/corpus_diff.py | 63 ++++++++++++ scripts/corpus_stats.py | 57 +++++++++++ 17 files changed, 411 insertions(+), 30 deletions(-) create mode 100644 DEV.md create mode 100644 fuzzing/coverage/report_template.gojson create mode 100644 scripts/corpus_diff.py create mode 100644 scripts/corpus_stats.py 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/fuzzing/config/config.go b/fuzzing/config/config.go index 87bea9dc..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"` diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index b6f32dbf..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", 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/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_worker.go b/fuzzing/fuzzer_worker.go index d058873f..2f3d3217 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -12,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" ) @@ -49,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 @@ -99,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 @@ -247,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))) } } } diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index 2e5892d6..a83aa523 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -3,6 +3,7 @@ package fuzzing import ( "fmt" "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" @@ -280,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 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/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/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]) From 5e2c6371178c470b6a73158bafd3727f6090568f Mon Sep 17 00:00:00 2001 From: s4nsec Date: Wed, 7 Aug 2024 17:36:24 +0400 Subject: [PATCH 55/55] Add latest medusa config --- medusa.json | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 medusa.json 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 + } +}