Skip to content

Commit

Permalink
feat: add execution trace for failed target contract deployments (#337)
Browse files Browse the repository at this point in the history
* feat: add execution trace for failed target contract deployments

* fix bugs in logging and improve trace output to console

---------

Co-authored-by: anishnaik <[email protected]>
  • Loading branch information
0xalpharush and anishnaik authored Mar 27, 2024
1 parent 382f7f2 commit e7b5e15
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 40 deletions.
9 changes: 8 additions & 1 deletion fuzzing/executiontracer/execution_tracer.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package executiontracer

import (
"math/big"

"github.com/crytic/medusa/chain"
"github.com/crytic/medusa/fuzzing/contracts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"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
Expand Down Expand Up @@ -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
}
}
}
}
Expand Down
67 changes: 47 additions & 20 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -333,15 +336,15 @@ 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 {
if len(fuzzer.contractDefinitions) == 1 {
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
}
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion fuzzing/fuzzer_hooks.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fuzzing

import (
"github.com/crytic/medusa/fuzzing/executiontracer"
"math/rand"

"github.com/crytic/medusa/chain"
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fuzzing

import (
"encoding/hex"
"github.com/crytic/medusa/fuzzing/executiontracer"
"math/big"
"math/rand"
"testing"
Expand Down Expand Up @@ -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)
}
Expand Down
8 changes: 7 additions & 1 deletion logging/log_buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 0 additions & 16 deletions logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down

0 comments on commit e7b5e15

Please sign in to comment.