From e7b5e153a2ee474fa5989c24c965827100886f2b Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 27 Mar 2024 11:26:56 -0500 Subject: [PATCH] feat: add execution trace for failed target contract deployments (#337) * feat: add execution trace for failed target contract deployments * fix bugs in logging and improve trace output to console --------- Co-authored-by: anishnaik --- fuzzing/executiontracer/execution_tracer.go | 9 ++- fuzzing/fuzzer.go | 67 +++++++++++++++------ fuzzing/fuzzer_hooks.go | 4 +- fuzzing/fuzzer_test.go | 3 +- logging/log_buffer.go | 8 ++- logging/logger.go | 16 ----- 6 files changed, 67 insertions(+), 40 deletions(-) diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index ed96dbe1..17ec57fe 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -1,6 +1,8 @@ package executiontracer import ( + "math/big" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/contracts" "github.com/ethereum/go-ethereum/common" @@ -8,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/vm" "golang.org/x/exp/slices" - "math/big" ) // CallWithExecutionTrace obtains an execution trace for a given call, on the provided chain, using the state @@ -118,6 +119,12 @@ func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *CallFra callFrame.ToContractName = toContract.Name() callFrame.ToContractAbi = &toContract.CompiledContract().Abi t.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 + } } } } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index dde89e9c..5697d1cf 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -2,11 +2,9 @@ package fuzzing import ( "context" + "errors" "fmt" - "github.com/crytic/medusa/fuzzing/coverage" - "github.com/crytic/medusa/logging" - "github.com/crytic/medusa/logging/colors" - "github.com/rs/zerolog" + "github.com/crytic/medusa/fuzzing/executiontracer" "math/big" "math/rand" "os" @@ -18,6 +16,11 @@ import ( "sync" "time" + "github.com/crytic/medusa/fuzzing/coverage" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" + "github.com/rs/zerolog" + "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/core/types" @@ -333,7 +336,7 @@ func (f *Fuzzer) createTestChain() (*chain.TestChain, error) { // all compiled contract definitions. This includes any successful compilations as a result of the Fuzzer.config // definitions, as well as those added by Fuzzer.AddCompilationTargets. The contract deployment order is defined by // the Fuzzer.config. -func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) error { +func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) { // Verify that target contracts is not empty. If it's empty, but we only have one contract definition, // we can infer the target contracts. Otherwise, we report an error. if len(fuzzer.config.Fuzzing.TargetContracts) == 0 { @@ -341,7 +344,7 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro fuzzer.config.Fuzzing.TargetContracts = []string{fuzzer.contractDefinitions[0].Name()} } else { return fmt.Errorf("missing target contracts (update fuzzing.targetContracts in the project config " + - "or use the --target-contracts CLI flag)") + "or use the --target-contracts CLI flag)"), nil } } @@ -357,20 +360,20 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro if len(contract.CompiledContract().Abi.Constructor.Inputs) > 0 { jsonArgs, ok := fuzzer.config.Fuzzing.ConstructorArgs[contractName] if !ok { - return fmt.Errorf("constructor arguments for contract %s not provided", contractName) + return fmt.Errorf("constructor arguments for contract %s not provided", contractName), nil } decoded, err := valuegeneration.DecodeJSONArgumentsFromMap(contract.CompiledContract().Abi.Constructor.Inputs, jsonArgs, deployedContractAddr) if err != nil { - return err + return err, nil } args = decoded } - // Constructor our deployment message/tx data field + // Construct our deployment message/tx data field msgData, err := contract.CompiledContract().GetDeploymentMessageData(args) if err != nil { - return fmt.Errorf("initial contract deployment failed for contract \"%v\", error: %v", contractName, err) + return fmt.Errorf("initial contract deployment failed for contract \"%v\", error: %v", contractName, err), nil } // If our project config has a non-zero balance for this target contract, retrieve it @@ -387,25 +390,45 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro // Create a new pending block we'll commit to chain block, err := testChain.PendingBlockCreate() if err != nil { - return err + return err, nil } // Add our transaction to the block // Add our transaction to the block err = testChain.PendingBlockAddTx(msg.ToCoreMessage()) if err != nil { - return err + return err, nil } // Commit the pending block to the chain, so it becomes the new head. err = testChain.PendingBlockCommit() if err != nil { - return err + return err, nil } - // Ensure our transaction succeeded + // Ensure our transaction succeeded and, if it did not, attach an execution trace to it and re-run it. + // The execution trace will be returned so that it can be provided to the user for debugging if block.MessageResults[0].Receipt.Status != types.ReceiptStatusSuccessful { - return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err) + // Create a call sequence element to represent the failed contract deployment tx + cse := calls.NewCallSequenceElement(nil, msg, 0, 0) + cse.ChainReference = &calls.CallSequenceElementChainReference{ + Block: block, + TransactionIndex: len(block.Messages) - 1, + } + + // Replay the execution trace for the failed contract deployment tx + err = cse.AttachExecutionTrace(testChain, fuzzer.contractDefinitions) + + // Throw an error if execution tracing threw an error or the trace is nil + if err != nil { + return fmt.Errorf("failed to attach execution trace to failed contract deployment tx: %v", err), nil + } + if cse.ExecutionTrace == nil { + return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err), nil + } + + // Return the execution error and the execution trace + return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err), cse.ExecutionTrace } // Record our deployed contract so the next config-specified constructor args can reference this @@ -421,10 +444,10 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro // If we did not find a contract corresponding to this item in the deployment order, we throw an error. if !found { - return fmt.Errorf("%v was specified in the target contracts but was not found in the compilation artifacts", contractName) + return fmt.Errorf("%v was specified in the target contracts but was not found in the compilation artifacts", contractName), nil } } - return nil + return nil, nil } // defaultCallSequenceGeneratorConfigFunc is a NewCallSequenceGeneratorConfigFunc which creates a @@ -641,9 +664,13 @@ func (f *Fuzzer) Start() error { // Set it up with our deployment/setup strategy defined by the fuzzer. f.logger.Info("Setting up base chain") - err = f.Hooks.ChainSetupFunc(f, baseTestChain) + err, trace := f.Hooks.ChainSetupFunc(f, baseTestChain) if err != nil { - f.logger.Error("Failed to initialize the test chain", err) + if trace != nil { + f.logger.Error("Failed to initialize the test chain", err, errors.New(trace.Log().ColorString())) + } else { + f.logger.Error("Failed to initialize the test chain", err) + } return err } @@ -825,7 +852,7 @@ func (f *Fuzzer) printExitingResults() { // Print the results of each individual test case. f.logger.Info("Fuzzer stopped, test results follow below ...") for _, testCase := range f.testCases { - f.logger.Info(testCase.LogMessage().Elements()...) + f.logger.Info(testCase.LogMessage().ColorString()) // Tally our pass/fail count. if testCase.Status() == TestCaseStatusPassed { diff --git a/fuzzing/fuzzer_hooks.go b/fuzzing/fuzzer_hooks.go index ea2d8486..cf458192 100644 --- a/fuzzing/fuzzer_hooks.go +++ b/fuzzing/fuzzer_hooks.go @@ -1,6 +1,7 @@ package fuzzing import ( + "github.com/crytic/medusa/fuzzing/executiontracer" "math/rand" "github.com/crytic/medusa/chain" @@ -41,7 +42,8 @@ type NewShrinkingValueMutatorFunc func(fuzzer *Fuzzer, valueSet *valuegeneration type NewCallSequenceGeneratorConfigFunc func(fuzzer *Fuzzer, valueSet *valuegeneration.ValueSet, randomProvider *rand.Rand) (*CallSequenceGeneratorConfig, error) // TestChainSetupFunc describes a function which sets up a test chain's initial state prior to fuzzing. -type TestChainSetupFunc func(fuzzer *Fuzzer, testChain *chain.TestChain) error +// An execution trace can also be returned in case of a deployment error for an improved debugging experience +type TestChainSetupFunc func(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) // CallSequenceTestFunc defines a method called after a fuzzing.FuzzerWorker sends another call in a types.CallSequence // during a fuzzing campaign. It returns a ShrinkCallSequenceRequest set, which represents a set of requests for diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index d0a47392..860ebd93 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -2,6 +2,7 @@ package fuzzing import ( "encoding/hex" + "github.com/crytic/medusa/fuzzing/executiontracer" "math/big" "math/rand" "testing" @@ -35,7 +36,7 @@ func TestFuzzerHooks(t *testing.T) { return existingSeqGenConfigFunc(fuzzer, valueSet, randomProvider) } existingChainSetupFunc := f.fuzzer.Hooks.ChainSetupFunc - f.fuzzer.Hooks.ChainSetupFunc = func(fuzzer *Fuzzer, testChain *chain.TestChain) error { + f.fuzzer.Hooks.ChainSetupFunc = func(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) { chainSetupOk = true return existingChainSetupFunc(fuzzer, testChain) } diff --git a/logging/log_buffer.go b/logging/log_buffer.go index 70abf4a4..761d7ab5 100644 --- a/logging/log_buffer.go +++ b/logging/log_buffer.go @@ -28,6 +28,12 @@ func (l *LogBuffer) Elements() []any { // String provides the non-colorized string representation of the LogBuffer func (l LogBuffer) String() string { - _, msg, _, _ := buildMsgs(l.elements) + _, msg, _, _ := buildMsgs(l.elements...) + return msg +} + +// ColorString provides the colorized string representation of the LogBuffer +func (l LogBuffer) ColorString() string { + msg, _, _, _ := buildMsgs(l.elements...) return msg } diff --git a/logging/logger.go b/logging/logger.go index 7a6c09e4..e52f4414 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -343,22 +343,6 @@ func chainStructuredLogInfoErrorsAndMsgs(structuredLog *zerolog.Event, unstructu // First, we need to create a formatted error string for unstructured output var errStr string for _, err := range errs { - // To make the formatting a little nicer, we will add a tab after each new line in the error so that - // errors can be better differentiated on unstructured channels - lines := make([]string, 0) - for i, line := range strings.Split(err.Error(), "\n") { - // Add a tab to the line only after the first new line in the error message - if i != 0 { - line = "\t" + line - } - lines = append(lines, line) - } - - // Update the error string to be based on the tabbed lines array - if len(lines) > 0 { - err = fmt.Errorf("%v", strings.Join(lines, "\n")) - } - // Append a bullet point and the formatted error to the error string errStr += "\n" + colors.BULLET_POINT + " " + err.Error() }