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

Execution traces for call sequences failing tests #105

Merged
merged 31 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f65bcb0
useless commit
anishnaik Mar 8, 2023
f046909
hacky way to get revert reason
anishnaik Mar 8, 2023
3960df8
Introduction of execution trace attaching and displaying for failed t…
Xenomega Mar 13, 2023
ac9b6be
* Added support for assert/reverts in ExecutionTrace
Xenomega Mar 13, 2023
7134493
Merge remote-tracking branch 'origin/master' into temp/event-logging
Xenomega Mar 13, 2023
600193e
Added event support to execution tracer and traces
Xenomega Mar 13, 2023
c59cd55
Removed old version of event emission test
Xenomega Mar 13, 2023
56d0f1d
Removed old call sequence event code
Xenomega Mar 13, 2023
b0a02a7
Improve indentation of execution traces for better readability
Xenomega Mar 13, 2023
b32085d
Fix lint issue
Xenomega Mar 13, 2023
30dfa43
Return data unpacking fix
Xenomega Mar 13, 2023
d904bc2
* Provided a wrapper for ExecuteCallSequenceIteratively, to simply re…
Xenomega Mar 15, 2023
7663797
Updated execution tracer to better record runtime bytecodes for "to" …
Xenomega Mar 15, 2023
863ed0f
Added self destruct test
Xenomega Mar 15, 2023
fc95b94
add traceAll config option
anishnaik Mar 15, 2023
a0c9e5f
updated formatting for call frames that do not execute code
anishnaik Mar 15, 2023
f26f976
fix bug in attachExecutionTraces
anishnaik Mar 15, 2023
45ecd9e
comment update
anishnaik Mar 15, 2023
4951cd6
lint updates
anishnaik Mar 15, 2023
8209b6a
another formatting update
anishnaik Mar 16, 2023
f7739b5
remove ChildCallFrames parameter and add as getter
anishnaik Mar 16, 2023
9b3e407
First pass at cheat code contract support in execution traces
Xenomega Mar 18, 2023
555f6c6
Merge branch 'master' into dev/execution-tracing
Xenomega Mar 18, 2023
77b2760
Added support for custom solidity errors (TODO: cleanup of execution …
Xenomega Mar 19, 2023
2e9044c
Cleaned up ABI error and event helpers by splitting them off to a com…
Xenomega Mar 19, 2023
704bcf2
* More cleanup of execution tracing code
Xenomega Mar 19, 2023
10c291a
Added a ConstructorArgsData field to execution tracing's CallFrame, t…
Xenomega Mar 19, 2023
d593592
Fix lint issue
Xenomega Mar 19, 2023
1365540
Add a test case for call argument resolution
Xenomega Mar 19, 2023
837b88b
Added support for proxy calls to execution tracer + tests
Xenomega Mar 21, 2023
56caf88
Updated doc comments
Xenomega Mar 21, 2023
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ This will use the `medusa.json` configuration in the current directory and begin

## Running Unit Tests

First, install [crytic-compile](https://github.com/crytic/crytic-compile), [solc-select](https://github.com/crytic/solc-select), and ensure you have `solc`, `truffle`, and `hardhat` available on your system.
First, install [crytic-compile](https://github.com/crytic/crytic-compile), [solc-select](https://github.com/crytic/solc-select), and ensure you have `solc` (version >=0.8.7), `truffle`, and `hardhat` available on your system.
anishnaik marked this conversation as resolved.
Show resolved Hide resolved

- From the root of the repository, invoke `go test -v ./...` on through command-line to run tests from all packages at or below the root.
- Or enter each package directory to run `go test -v .` to test the immediate package.
Expand Down
72 changes: 56 additions & 16 deletions chain/test_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,23 +411,43 @@ func (t *TestChain) BlockHashFromNumber(blockNumber uint64) (common.Hash, error)
}
}

// StateAfterBlockNumber obtains the Ethereum world state after processing all transactions in the provided block
// number. Returns the state, or an error if one occurs.
func (t *TestChain) StateAfterBlockNumber(blockNumber uint64) (*state.StateDB, error) {
// StateFromRoot obtains a state from a given state root hash.
// Returns the state, or an error if one occurred.
func (t *TestChain) StateFromRoot(root common.Hash) (*state.StateDB, error) {
// Load our state from the database
stateDB, err := state.New(root, t.stateDatabase, nil)
if err != nil {
return nil, err
}
return stateDB, nil
}

// StateRootAfterBlockNumber obtains the Ethereum world state root hash after processing all transactions in the
// provided block number. Returns the state, or an error if one occurs.
func (t *TestChain) StateRootAfterBlockNumber(blockNumber uint64) (common.Hash, error) {
// If our block number references something too new, return an error
if blockNumber > t.HeadBlockNumber() {
return nil, fmt.Errorf("could not obtain post-state for block number %d because it exceeds the current head block number %d", blockNumber, t.HeadBlockNumber())
return common.Hash{}, fmt.Errorf("could not obtain post-state for block number %d because it exceeds the current head block number %d", blockNumber, t.HeadBlockNumber())
}

// Obtain our closest internally committed block
_, closestBlock := t.fetchClosestInternalBlock(blockNumber)

// Load our state from the database
stateDB, err := state.New(closestBlock.Header.Root, t.stateDatabase, nil)
// Return our state root hash
return closestBlock.Header.Root, nil
}

// StateAfterBlockNumber obtains the Ethereum world state after processing all transactions in the provided block
// number. Returns the state, or an error if one occurs.
func (t *TestChain) StateAfterBlockNumber(blockNumber uint64) (*state.StateDB, error) {
// Obtain our block's post-execution state root hash
root, err := t.StateRootAfterBlockNumber(blockNumber)
if err != nil {
return nil, err
}
return stateDB, nil

// Load our state from the database
return t.StateFromRoot(root)
}

// RevertToBlockNumber sets the head of the chain to the block specified by the provided block number and reloads
Expand Down Expand Up @@ -486,24 +506,38 @@ func (t *TestChain) RevertToBlockNumber(blockNumber uint64) error {
}

// CallContract performs a message call over the current test chain state and obtains a core.ExecutionResult.
// This is similar to the CallContract method provided by Ethereum for use in calling pure/view functions.
// This is similar to the CallContract method provided by Ethereum for use in calling pure/view functions, as it
// executed a transaction without committing any changes, instead discarding them.
// It takes an optional state argument, which is the state to execute the message over. If not provided, the
// current pending state (or committed state if none is pending) will be used instead.
// The state executed over may be a pending block state.
func (t *TestChain) CallContract(msg core.Message) (*core.ExecutionResult, error) {
// Obtain our state snapshot (note: this is different from the test chain snapshot)
snapshot := t.state.Snapshot()
func (t *TestChain) CallContract(msg core.Message, state *state.StateDB, additionalTracers ...vm.EVMLogger) (*core.ExecutionResult, error) {
// If our provided state is nil, use our current chain state.
if state == nil {
state = t.state
}

// Obtain our state snapshot to revert any changes after our call
snapshot := state.Snapshot()

// Set infinite balance to the fake caller account
from := t.state.GetOrNewStateObject(msg.From())
from := state.GetOrNewStateObject(msg.From())
from.SetBalance(math.MaxBig256)

// Create our transaction and block contexts for the vm
txContext := core.NewEVMTxContext(msg)
blockContext := newTestChainBlockContext(t, t.Head().Header)

// Create a new call tracer router that incorporates any additional tracers provided just for this call, while
// still calling our internal tracers.
extendedTracerRouter := NewTestChainTracerRouter()
extendedTracerRouter.AddTracer(t.callTracerRouter)
extendedTracerRouter.AddTracers(additionalTracers...)

anishnaik marked this conversation as resolved.
Show resolved Hide resolved
// Create our EVM instance.
evm := vm.NewEVM(blockContext, txContext, t.state, t.chainConfig, vm.Config{
evm := vm.NewEVM(blockContext, txContext, state, t.chainConfig, vm.Config{
Debug: true,
Tracer: t.callTracerRouter,
Tracer: extendedTracerRouter,
NoBaseFee: true,
ConfigExtensions: t.vmConfigExtensions,
})
Expand All @@ -515,7 +549,7 @@ func (t *TestChain) CallContract(msg core.Message) (*core.ExecutionResult, error
res, err := core.NewStateTransition(evm, msg, gasPool).TransitionDb()

// Revert to our state snapshot to undo any changes.
t.state.RevertToSnapshot(snapshot)
state.RevertToSnapshot(snapshot)

return res, err
}
Expand Down Expand Up @@ -628,6 +662,9 @@ func (t *TestChain) PendingBlockAddTx(message core.Message) error {
return errors.New("could not add tx to the chain's pending block because no pending block was created")
}

// Obtain our state root hash prior to execution.
previousStateRoot := t.pendingBlock.Header.Root

// Create a gas pool indicating how much gas can be spent executing the transaction.
gasPool := new(core.GasPool).AddGas(t.pendingBlock.Header.GasLimit - t.pendingBlock.Header.GasUsed)

Expand Down Expand Up @@ -657,6 +694,8 @@ func (t *TestChain) PendingBlockAddTx(message core.Message) error {

// Create our message result
messageResult := &chainTypes.MessageResults{
PreStateRoot: previousStateRoot,
PostStateRoot: common.Hash{},
ExecutionResult: executionResult,
Receipt: receipt,
AdditionalResults: make(map[string]any, 0),
Expand Down Expand Up @@ -684,10 +723,11 @@ func (t *TestChain) PendingBlockAddTx(message core.Message) error {
// Update our block's bloom filter
t.pendingBlock.Header.Bloom.Add(receipt.Bloom.Bytes())

// Update the header's state root hash
// Update the header's state root hash, as well as our message result's
// Note: You could also retrieve the root without committing by using
// state.IntermediateRoot(config.IsEIP158(parentBlockNumber)).
t.pendingBlock.Header.Root = root
messageResult.PostStateRoot = root

// Update our block's transactions and results.
t.pendingBlock.Messages = append(t.pendingBlock.Messages, message)
Expand Down
7 changes: 7 additions & 0 deletions chain/types/message_results.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package types

import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
)

// MessageResults represents metadata obtained from the execution of a CallMessage in a Block.
// This contains results such as contracts deployed, and other variables tracked by a chain.TestChain.
type MessageResults struct {
// PreStateRoot refers to the state root hash prior to the execution of this transaction.
PreStateRoot common.Hash

// PostStateRoot refers to the state root hash after the execution of this transaction.
PostStateRoot common.Hash

// ExecutionResult describes the core.ExecutionResult returned after processing a given call.
ExecutionResult *core.ExecutionResult

Expand Down
80 changes: 80 additions & 0 deletions chain/types/solidity_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package types

import (
"bytes"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/core/vm"
"math/big"
)

// An enum is defined below providing all `Panic(uint)` error codes returned in return data when the VM encounters
// an error in some cases.
// Reference: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
const (
PanicCodeCompilerInserted = 0x00
PanicCodeAssertFailed = 0x01
PanicCodeArithmeticUnderOverflow = 0x11
PanicCodeDivideByZero = 0x12
PanicCodeEnumTypeConversionOutOfBounds = 0x21
PanicCodeIncorrectStorageAccess = 0x22
PanicCodePopEmptyArray = 0x31
PanicCodeOutOfBoundsArrayAccess = 0x32
PanicCodeAllocateTooMuchMemory = 0x41
PanicCodeCallUninitializedVariable = 0x51
)

// GetSolidityPanicCode obtains a panic code from a VM error and return data, if possible.
// If the error and return data are not representative of a Panic, then nil is returned.
func GetSolidityPanicCode(returnError error, returnData []byte, backwardsCompatible bool) *big.Int {
// If this method is backwards compatible with older solidity, there is no panic code, and we simply look
// for a specific error that used to represent assertion failures.
_, hitInvalidOpcode := returnError.(*vm.ErrInvalidOpCode)
if backwardsCompatible && hitInvalidOpcode {
return big.NewInt(PanicCodeAssertFailed)
}

// Verify we have a revert, and our return data fits exactly the selector + uint256
if returnError == vm.ErrExecutionReverted && len(returnData) == 4+32 {
uintType, _ := abi.NewType("uint256", "", nil)
panicReturnDataAbi := abi.NewMethod("Panic", "Panic", abi.Function, "", false, false, []abi.Argument{
{Name: "", Type: uintType, Indexed: false},
}, abi.Arguments{})

// Verify the return data starts with the correct selector, then unpack the arguments.
if bytes.Equal(returnData[:4], panicReturnDataAbi.ID) {
values, err := panicReturnDataAbi.Inputs.Unpack(returnData[4:])

// If they unpacked without issue, read the panic code.
if err == nil && len(values) > 0 {
panicCode := values[0].(*big.Int)
return panicCode
}
}
}
return nil
}

// GetSolidityRevertErrorString obtains an error message from a VM error and return data, if possible.
// If the error and return data are not representative of an Error, then nil is returned.
func GetSolidityRevertErrorString(returnError error, returnData []byte) *string {
// Verify we have a revert, and our return data fits the selector + additional data.
if returnError == vm.ErrExecutionReverted && len(returnData) > 4 {
stringType, _ := abi.NewType("string", "", nil)
errorReturnDataAbi := abi.NewMethod("Error", "Error", abi.Function, "", false, false, []abi.Argument{
{Name: "", Type: stringType, Indexed: false},
}, abi.Arguments{})

// Verify the return data starts with the correct selector, then unpack the arguments.
if bytes.Equal(returnData[:4], errorReturnDataAbi.ID) {
values, err := errorReturnDataAbi.Inputs.Unpack(returnData[4:])

// If they unpacked without issue, read the error string.
if err == nil && len(values) > 0 {
errorMessage := values[0].(string)
return &errorMessage
}
}
}

return nil
}
60 changes: 56 additions & 4 deletions fuzzing/calls/call_sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/trailofbits/medusa/chain"
"github.com/trailofbits/medusa/fuzzing/executiontracer"
"github.com/trailofbits/medusa/utils"
"strconv"
"strings"
Expand All @@ -18,17 +20,38 @@ import (
// CallSequence describes a sequence of calls sent to a chain.
type CallSequence []*CallSequenceElement

// AttachExecutionTraces takes a given chain which executed the call sequence, and a list of contract definitions,
// and it replays each call of the sequence with an execution tracer attached to it, it then sets each
// CallSequenceElement.ExecutionTrace to the resulting trace.
// Returns an error if one occurred.
func (cs CallSequence) AttachExecutionTraces(chain *chain.TestChain, contractDefinitions fuzzingTypes.Contracts) error {
// For each call sequence element, attach an execution trace.
for _, cse := range cs {
err := cse.AttachExecutionTrace(chain, contractDefinitions)
if err != nil {
return nil
}
}
return nil
}

// String returns a displayable string representing the CallSequence.
func (cs CallSequence) String() string {
// If we have an empty call sequence, return a special string
if len(cs) == 0 {
return "<none>"
}

// Construct a list of strings for each CallSequenceElement.
elementStrings := make([]string, len(cs))
for i := 0; i < len(elementStrings); i++ {
elementStrings[i] = fmt.Sprintf("%d) %s", i+1, cs[i].String())
// Construct a list of strings for each call made in the sequence
var elementStrings []string
for i := 0; i < len(cs); i++ {
// Add the string representing the call
elementStrings = append(elementStrings, fmt.Sprintf("%d) %s", i+1, cs[i].String()))

// If we have an execution trace attached, print information about it.
if cs[i].ExecutionTrace != nil {
elementStrings = append(elementStrings, cs[i].ExecutionTrace.String())
}
}

// Join each element with new lines and return it.
Expand Down Expand Up @@ -111,6 +134,9 @@ type CallSequenceElement struct {
// committed to its underlying chain if this is a CallSequenceElement was just executed. Additional transactions
// may be included before the block is committed. This reference will remain compatible after the block finalizes.
ChainReference *CallSequenceElementChainReference `json:"-"`

// ExecutionTrace represents a verbose execution trace collected. Nil if an execution trace was not collected.
ExecutionTrace *executiontracer.ExecutionTrace `json:"-"`
}

// NewCallSequenceElement returns a new CallSequenceElement struct to track a single call made within a CallSequence.
Expand All @@ -121,6 +147,7 @@ func NewCallSequenceElement(contract *fuzzingTypes.Contract, call *CallMessage,
BlockNumberDelay: blockNumberDelay,
BlockTimestampDelay: blockTimestampDelay,
ChainReference: nil,
ExecutionTrace: nil,
}
return callSequenceElement
}
Expand All @@ -140,6 +167,7 @@ func (cse *CallSequenceElement) Clone() (*CallSequenceElement, error) {
BlockNumberDelay: cse.BlockNumberDelay,
BlockTimestampDelay: cse.BlockTimestampDelay,
ChainReference: cse.ChainReference,
ExecutionTrace: cse.ExecutionTrace,
}
return clone, nil
}
Expand Down Expand Up @@ -202,6 +230,30 @@ func (cse *CallSequenceElement) String() string {
)
}

// AttachExecutionTrace takes a given chain which executed the call sequence element, and a list of contract definitions,
// and it replays the call with an execution tracer attached to it, it then sets CallSequenceElement.ExecutionTrace to
// the resulting trace.
// Returns an error if one occurred.
anishnaik marked this conversation as resolved.
Show resolved Hide resolved
func (cse *CallSequenceElement) AttachExecutionTrace(chain *chain.TestChain, contractDefinitions fuzzingTypes.Contracts) error {
// Verify the element has been executed before.
if cse.ChainReference == nil {
return fmt.Errorf("failed to resolve execution trace as the chain reference is nil, indicating the call sequence element has never been executed")
}

// Obtain the state prior to executing this transaction.
state, err := chain.StateFromRoot(cse.ChainReference.MessageResults().PreStateRoot)
if err != nil {
return fmt.Errorf("failed to resolve execution trace due to error loading root hash from database: %v", err)
}

// Perform our call with the given trace
_, cse.ExecutionTrace, err = executiontracer.CallWithExecutionTrace(chain, contractDefinitions, cse.Call, state)
if err != nil {
return fmt.Errorf("failed to resolve execution trace due to error replaying the call: %v", err)
}
return nil
}

// CallSequenceElementChainReference references the inclusion of a CallSequenceElement's underlying call being
// included in a block as a transaction.
type CallSequenceElementChainReference struct {
Expand Down
16 changes: 16 additions & 0 deletions fuzzing/calls/call_sequence_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,19 @@ func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc Exe
}
return callSequenceExecuted, nil
}

// ExecuteCallSequence executes a provided CallSequence on the provided chain.
// It returns the slice of the call sequence which was tested, and an error if one occurred.
// If no error occurred, it can be expected that the returned call sequence contains all elements originally provided.
func ExecuteCallSequence(chain *chain.TestChain, callSequence CallSequence) (CallSequence, error) {
// Execute our sequence with a simple fetch operation provided to obtain each element.
fetchElementFunc := func(currentIndex int) (*CallSequenceElement, error) {
if currentIndex < len(callSequence) {
return callSequence[currentIndex], nil
}
return nil, nil
}

// Execute our provided call sequence iteratively.
return ExecuteCallSequenceIteratively(chain, fetchElementFunc, nil)
}
Loading