Skip to content

Commit

Permalink
Added support for proxy calls to execution tracer + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Xenomega committed Mar 21, 2023
1 parent 1365540 commit 837b88b
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 44 deletions.
8 changes: 4 additions & 4 deletions fuzzing/executiontracer/execution_trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,15 @@ func (t *ExecutionTrace) generateCallFrameEnterString(callFrame *CallFrame) stri
// If we executed code, attach additional context such as the contract name, method, etc.
if callFrame.IsProxyCall() {
if callFrame.ExecutedCode {
return fmt.Sprintf("[%v] %v -> %v.%v(%v) (to=%v, value=%v, sender=%v)", callType, proxyContractName, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String())
return fmt.Sprintf("[%v] %v -> %v.%v(%v) (addr=%v, code=%v, value=%v, sender=%v)", callType, proxyContractName, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CodeAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String())
} else {
return fmt.Sprintf("[%v] (to=%v, value=%v, sender=%v)", callType, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String())
return fmt.Sprintf("[%v] (addr=%v, value=%v, sender=%v)", callType, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String())
}
} else {
if callFrame.ExecutedCode {
return fmt.Sprintf("[%v] %v.%v(%v) (to=%v, value=%v, sender=%v)", callType, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String())
return fmt.Sprintf("[%v] %v.%v(%v) (addr=%v, value=%v, sender=%v)", callType, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String())
} else {
return fmt.Sprintf("[%v] (to=%v, value=%v, sender=%v)", callType, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String())
return fmt.Sprintf("[%v] (addr=%v, value=%v, sender=%v)", callType, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String())
}
}
}
Expand Down
73 changes: 35 additions & 38 deletions fuzzing/executiontracer/execution_tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,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, codeAddress common.Address, inputData []byte, isContractCreation bool, value *big.Int) {
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{
SenderAddress: fromAddress,
Expand All @@ -150,7 +150,7 @@ func (t *ExecutionTracer) captureEnteredCallFrame(fromAddress common.Address, to
ToContractAbi: nil,
ToInitBytecode: nil,
ToRuntimeBytecode: nil,
CodeAddress: codeAddress,
CodeAddress: toAddress, // Note: Set temporarily, overwritten if code executes (in CaptureState).
CodeContractName: "",
CodeContractAbi: nil,
CodeRuntimeBytecode: nil,
Expand All @@ -165,24 +165,11 @@ func (t *ExecutionTracer) captureEnteredCallFrame(fromAddress common.Address, to
ParentCallFrame: t.currentCallFrame,
}

// Set our known information about the code we're executing.
// If this is a contract creation, set the init bytecode for this call frame to the input data.
if isContractCreation {
callFrameData.ToInitBytecode = inputData
} else {
callFrameData.ToRuntimeBytecode = t.evm.StateDB.GetCode(callFrameData.ToAddress)
}

// If we are performing a proxy call, we use code from another address (which we can expect to exist), and fetch
// that code, so we can later match the contract definition. Otherwise, we record the same bytecode as "to".
if callFrameData.CodeAddress != callFrameData.ToAddress {
callFrameData.CodeRuntimeBytecode = t.evm.StateDB.GetCode(callFrameData.CodeAddress)
} else {
callFrameData.CodeRuntimeBytecode = callFrameData.ToRuntimeBytecode
}

// Resolve our contract definitions on the call frame data, if they have not been.
t.resolveCallFrameContractDefinitions(callFrameData)

// Set our current call frame in our trace
if t.trace.TopLevelCallFrame == nil {
t.trace.TopLevelCallFrame = callFrameData
Expand All @@ -195,28 +182,28 @@ func (t *ExecutionTracer) captureEnteredCallFrame(fromAddress common.Address, to
// captureExitedCallFrame is a helper method used when a call frame is exited, to record information about it.
func (t *ExecutionTracer) 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.
callFrameData := t.currentCallFrame
if err == nil {
if callFrameData.ToRuntimeBytecode == nil {
callFrameData.ToRuntimeBytecode = t.evm.StateDB.GetCode(callFrameData.ToAddress)
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.evm.StateDB.GetCode(t.currentCallFrame.ToAddress)
}
if callFrameData.CodeRuntimeBytecode == nil {
// Optimization: If the "to" and "code" addresses match, we can simply set our "code" already fetched "to"
// runtime bytecode.
if callFrameData.CodeAddress == callFrameData.ToAddress {
callFrameData.CodeRuntimeBytecode = callFrameData.ToRuntimeBytecode
} else {
callFrameData.CodeRuntimeBytecode = t.evm.StateDB.GetCode(callFrameData.CodeAddress)
}
}
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.evm.StateDB.GetCode(t.currentCallFrame.CodeAddress)
}
}

// Resolve our contract definitions on the call frame data, if they have not been.
t.resolveCallFrameContractDefinitions(callFrameData)
t.resolveCallFrameContractDefinitions(t.currentCallFrame)

// Set our information for this call frame
callFrameData.ReturnData = slices.Clone(output)
callFrameData.ReturnError = err
t.currentCallFrame.ReturnData = slices.Clone(output)
t.currentCallFrame.ReturnError = err

// We're exiting the current frame, so set our current call frame to the parent
t.currentCallFrame = t.currentCallFrame.ParentCallFrame
Expand All @@ -228,7 +215,7 @@ func (t *ExecutionTracer) CaptureStart(env *vm.EVM, from common.Address, to comm
t.evm = env

// Capture that a new call frame was entered.
t.captureEnteredCallFrame(from, to, to, input, create, value)
t.captureEnteredCallFrame(from, to, input, create, value)
}

// CaptureEnd is called after a call to finalize tracing completes for the top of a call frame, as defined by vm.EVMLogger.
Expand All @@ -243,7 +230,7 @@ func (t *ExecutionTracer) CaptureEnter(typ vm.OpCode, from common.Address, to co
t.callDepth++

// Capture that a new call frame was entered.
t.captureEnteredCallFrame(from, to, to, input, typ == vm.CREATE || typ == vm.CREATE2, value)
t.captureEnteredCallFrame(from, to, input, typ == vm.CREATE || typ == vm.CREATE2, value)
}

// CaptureExit is called upon exiting of the call frame, as defined by vm.EVMLogger.
Expand All @@ -263,15 +250,25 @@ func (t *ExecutionTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64
}
t.onNextCaptureState = nil

// Obtain our current call frame.
callFrameData := t.currentCallFrame
// 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 sender/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 {
t.currentCallFrame.SenderAddress = scope.Contract.CallerAddress
t.currentCallFrame.ToAddress = scope.Contract.Address()
if scope.Contract.CodeAddr != nil {
t.currentCallFrame.CodeAddress = *scope.Contract.CodeAddr
}

// Since we are executing an opcode, we can mark that we have executed code in this call frame
callFrameData.ExecutedCode = true
// 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
}

// If we encounter a SELFDESTRUCT operation, record the operation.
if op == vm.SELFDESTRUCT {
callFrameData.SelfDestructed = true
t.currentCallFrame.SelfDestructed = true
}

// If a log operation occurred, add a deferred operation to capture it.
Expand Down
3 changes: 2 additions & 1 deletion fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ func TestExecutionTraces(t *testing.T) {
expectedMessagesPerTest := map[string][]string{
"testdata/contracts/execution_tracing/call_and_deployment_args.sol": {"Hello from deployment args!", "Hello from call args!"},
"testdata/contracts/execution_tracing/cheatcodes.sol": {"StdCheats.toString(true)"},
"testdata/contracts/execution_tracing/event_emission.sol": {"[event] TestEvent", "[event] TestIndexedEvent", "[event] TestMixedEvent", "Hello from an event emission!"},
"testdata/contracts/execution_tracing/event_emission.sol": {"TestEvent", "TestIndexedEvent", "TestMixedEvent", "Hello from event args!"},
"testdata/contracts/execution_tracing/proxy_call.sol": {"TestContract -> InnerDeploymentContract.setXY", "Hello from proxy call args!"},
"testdata/contracts/execution_tracing/revert_custom_error.sol": {"CustomError", "Hello from a custom error!"},
"testdata/contracts/execution_tracing/revert_reasons.sol": {"RevertingContract was called and reverted."},
"testdata/contracts/execution_tracing/self_destruct.sol": {"[selfdestruct]", "[assertion failed]"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ contract TestContract {

emit TestEvent(value);
emit TestIndexedEvent(value);
emit TestMixedEvent(msg.sender, value, 7, "Hello from an event emission!");
emit TestMixedEvent(msg.sender, value, 7, "Hello from event args!");
assert(x % 2 == 0);
}
}
28 changes: 28 additions & 0 deletions fuzzing/testdata/contracts/execution_tracing/proxy_call.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// This contract ensures the fuzzer's execution tracing can trace proxy calls appropriately.
contract InnerDeploymentContract {
uint x;
uint y;
function setXY(uint _x, uint _y, string memory s) public payable {
x = _x;
y = _y;
}
}

contract TestContract {
uint x;
uint y;
InnerDeploymentContract i;

constructor() public {
i = new InnerDeploymentContract();
}

function testDelegateCall() public returns (address) {
// Perform a delegate call to set our variables in this contract.
(bool success, bytes memory data) = address(i).delegatecall(abi.encodeWithSignature("setXY(uint256,uint256,string)", 123, 321, "Hello from proxy call args!"));

// Trigger an assertion failure (ending the test), if data was set successfully.
// If we don't hit this assertion failure, something is wrong.
assert(!(x == 123 && y == 321));
}
}

0 comments on commit 837b88b

Please sign in to comment.