Skip to content

Commit

Permalink
console.log go brrr (#193)
Browse files Browse the repository at this point in the history
Introduce `console.log` functionality with string formatting capabilities.

---------

Co-authored-by: David Pokora <[email protected]>
  • Loading branch information
anishnaik and Xenomega authored Aug 15, 2023
1 parent 4de61e8 commit 7d2e16c
Show file tree
Hide file tree
Showing 9 changed files with 1,898 additions and 47 deletions.
27 changes: 25 additions & 2 deletions chain/cheat_code_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,30 @@ type cheatCodeRawReturnData struct {
Err error
}

// getCheatCodeProviders obtains a cheatCodeTracer (used to power cheat code analysis) and associated CheatCodeContract
// objects linked to the tracer (providing on-chain callable methods as an entry point). These objects are attached to
// the TestChain to enable cheat code functionality.
// Returns the tracer and associated pre-compile contracts, or an error, if one occurred.
func getCheatCodeProviders() (*cheatCodeTracer, []*CheatCodeContract, error) {
// Create a cheat code tracer and attach it to the chain.
tracer := newCheatCodeTracer()

// Obtain our standard cheat code pre-compile
stdCheatCodeContract, err := getStandardCheatCodeContract(tracer)
if err != nil {
return nil, nil, err
}

// Obtain the console.log pre-compile
consoleCheatCodeContract, err := getConsoleLogCheatCodeContract(tracer)
if err != nil {
return nil, nil, err
}

// Return the tracer and precompiles
return tracer, []*CheatCodeContract{stdCheatCodeContract, consoleCheatCodeContract}, nil
}

// newCheatCodeContract returns a new precompiledContract which uses the attached cheatCodeTracer for execution
// context.
func newCheatCodeContract(tracer *cheatCodeTracer, address common.Address, name string) *CheatCodeContract {
Expand Down Expand Up @@ -98,7 +122,7 @@ func (c *CheatCodeContract) Abi() *abi.ABI {
}

// addMethod adds a new method to the precompiled contract.
// Returns an error if one occurred.
// Throws a panic if either the name is the empty string or the handler is nil.
func (c *CheatCodeContract) addMethod(name string, inputs abi.Arguments, outputs abi.Arguments, handler cheatCodeMethodHandler) {
// Verify a method name was provided
if name == "" {
Expand All @@ -117,7 +141,6 @@ func (c *CheatCodeContract) addMethod(name string, inputs abi.Arguments, outputs
method: method,
handler: handler,
}

// Add the method to the ABI.
// Note: Normally the key here should be the method name, not sig. But cheat code contracts have duplicate
// method names with different parameter types, so we use this so they don't override.
Expand Down
124 changes: 124 additions & 0 deletions chain/console_log_cheat_code_contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package chain

import (
"github.com/crytic/medusa/utils"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"strconv"
)

// ConsoleLogContractAddress is the address for the console.log precompile contract
var ConsoleLogContractAddress = common.HexToAddress("0x000000000000000000636F6e736F6c652e6c6f67")

// getConsoleLogCheatCodeContract obtains a CheatCodeContract which implements the console.log functions.
// Returns the precompiled contract, or an error if there is one.
func getConsoleLogCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, error) {
// Create a new precompile to add methods to.
contract := newCheatCodeContract(tracer, ConsoleLogContractAddress, "Console")

// Define all the ABI types needed for console.log functions
typeUint256, err := abi.NewType("uint256", "", nil)
if err != nil {
return nil, err
}
typeInt256, err := abi.NewType("int256", "", nil)
if err != nil {
return nil, err
}
typeString, err := abi.NewType("string", "", nil)
if err != nil {
return nil, err
}
typeBool, err := abi.NewType("bool", "", nil)
if err != nil {
return nil, err
}
typeAddress, err := abi.NewType("address", "", nil)
if err != nil {
return nil, err
}
typeBytes, err := abi.NewType("bytes", "", nil)
if err != nil {
return nil, err
}

// We will store all the fixed byte (e.g. byte1, byte2) in a mapping
const numFixedByteTypes = 32
fixedByteTypes := make(map[int]abi.Type, numFixedByteTypes)
for i := 1; i <= numFixedByteTypes; i++ {
byteString := "bytes" + strconv.FormatInt(int64(i), 10)
fixedByteTypes[i], err = abi.NewType(byteString, "", nil)
if err != nil {
return nil, err
}
}

// We have a few special log function signatures outside all the permutations of (string, uint256, bool, address).
// These include log(int256), log(bytes), log(bytesX), and log(string, uint256). So, we will manually create these
// signatures and then programmatically iterate through all the permutations.

// Note that none of the functions actually do anything - they just have to be callable so that the execution
// traces can show the arguments that the user wants to log!

// log(int256): Log an int256
contract.addMethod("log", abi.Arguments{{Type: typeInt256}}, abi.Arguments{},
func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) {
return nil, nil
},
)

// log(bytes): Log bytes
contract.addMethod("log", abi.Arguments{{Type: typeBytes}}, abi.Arguments{},
func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) {
return nil, nil
},
)

// Now, we will add the logBytes1, logBytes2, and so on in a loop
for i := 1; i <= numFixedByteTypes; i++ {
// Create local copy of abi argument
fixedByteType := fixedByteTypes[i]

// Add the method
contract.addMethod("log", abi.Arguments{{Type: fixedByteType}}, abi.Arguments{},
func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) {
return nil, nil
},
)
}

// log(string, int256): Log string with an int where the string could be formatted
contract.addMethod("log", abi.Arguments{{Type: typeString}, {Type: typeInt256}}, abi.Arguments{},
func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) {
return nil, nil
},
)

// These are the four parameter types that console.log() accepts
choices := abi.Arguments{{Type: typeUint256}, {Type: typeString}, {Type: typeBool}, {Type: typeAddress}}

// Create all possible permutations (with repetition) where the number of choices increases from 1...len(choices)
permutations := make([]abi.Arguments, 0)
for n := 1; n <= len(choices); n++ {
nextSetOfPermutations := utils.PermutationsWithRepetition(choices, n)
for _, permutation := range nextSetOfPermutations {
permutations = append(permutations, permutation)
}
}

// Iterate across each permutation to add their associated event and function handler
for i := 0; i < len(permutations); i++ {
// Make a local copy of the current permutation
permutation := permutations[i]

// Create the function handler
contract.addMethod("log", permutation, abi.Arguments{},
func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) {
return nil, nil
},
)
}

// Return our precompile contract information.
return contract, nil
}
24 changes: 4 additions & 20 deletions chain/cheat_codes.go → chain/standard_cheat_code_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,14 @@ import (
"strings"
)

// getCheatCodeProviders obtains a cheatCodeTracer (used to power cheat code analysis) and associated CheatCodeContract
// objects linked to the tracer (providing on-chain callable methods as an entry point). These objects are attached to
// the TestChain to enable cheat code functionality.
// Returns the tracer and associated pre-compile contracts, or an error, if one occurred.
func getCheatCodeProviders() (*cheatCodeTracer, []*CheatCodeContract, error) {
// Create a cheat code tracer and attach it to the chain.
tracer := newCheatCodeTracer()

// Obtain our cheat code pre-compiles
stdCheatCodeContract, err := getStandardCheatCodeContract(tracer)
if err != nil {
return nil, nil, err
}

// Return the tracer and precompiles
return tracer, []*CheatCodeContract{stdCheatCodeContract}, nil
}
// StandardCheatcodeContractAddress is the address for the standard cheatcode contract
var StandardCheatcodeContractAddress = common.HexToAddress("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D")

// getStandardCheatCodeContract obtains a CheatCodeContract which implements common cheat codes.
// Returns the precompiled contract, or an error if one occurs.
func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, error) {
// Define our address for this precompile contract, then create a new precompile to add methods to.
contractAddress := common.HexToAddress("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D")
contract := newCheatCodeContract(tracer, contractAddress, "StdCheats")
// Create a new precompile to add methods to.
contract := newCheatCodeContract(tracer, StandardCheatcodeContractAddress, "StdCheats")

// Define some basic ABI argument types
typeAddress, err := abi.NewType("address", "", nil)
Expand Down
20 changes: 10 additions & 10 deletions compilation/abiutils/solidity_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,25 +113,25 @@ func GetPanicReason(panicCode uint64) string {
// Switch on panic code
switch panicCode {
case PanicCodeCompilerInserted:
return "compiler inserted panic"
return "panic: compiler inserted panic"
case PanicCodeAssertFailed:
return "assertion failed"
return "panic: assertion failed"
case PanicCodeArithmeticUnderOverflow:
return "arithmetic underflow"
return "panic: arithmetic underflow"
case PanicCodeDivideByZero:
return "division by zero"
return "panic: division by zero"
case PanicCodeEnumTypeConversionOutOfBounds:
return "enum access out of bounds"
return "panic: enum access out of bounds"
case PanicCodeIncorrectStorageAccess:
return "incorrect storage access"
return "panic: incorrect storage access"
case PanicCodePopEmptyArray:
return "pop on empty array"
return "panic: pop on empty array"
case PanicCodeOutOfBoundsArrayAccess:
return "out of bounds array access"
return "panic: out of bounds array access"
case PanicCodeAllocateTooMuchMemory:
return "overallocation of memory"
return "panic; overallocation of memory"
case PanicCodeCallUninitializedVariable:
return "call on uninitialized variable"
return "panic: call on uninitialized variable"
default:
return fmt.Sprintf("unknown panic code(%v)", panicCode)
}
Expand Down
Loading

0 comments on commit 7d2e16c

Please sign in to comment.