Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add execution trace for failed target contract deployments #337

Merged
merged 3 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
anishnaik marked this conversation as resolved.
Show resolved Hide resolved
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
Loading