Skip to content

Commit

Permalink
Skip failed contract matching + misc updates (#54)
Browse files Browse the repository at this point in the history
* Migrates crytic-compile support to leverage the solc export format, rather than standard.
* Allows ignoring errors related to failing to match deployed bytecode to a known contract definition
* Fixes an incorrect event emission in TestChain.
* Changed CompiledContract to store InitBytecode and RuntimeBytecode as byte slices rather than hex strings.
* Extended ContractMethodID to also use source path for uniqueness.
* Exposed a getter for TestChain.state so it can be queried in tests.
* Sorts final test case output by status/ID when exiting the program, so it's more readable.

Co-authored-by: anishnaik <anish.r.naik@gmail.com>
  • Loading branch information
Xenomega and anishnaik committed Dec 7, 2022
1 parent fd18ba4 commit 612469e
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 167 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ It provides parallelized fuzz testing of smart contracts through CLI, or its Go
- ✔️**Coverage collecting**: Coverage increasing call sequences are stored in the corpus
-**Coverage guided**: Coverage increasing call sequences from the corpus are mutated to further guide the fuzzing campaign
- ✔️**Extensible low-level testing API** through events and hooks provided throughout the fuzzer, workers, and test chains.
-**Extensible high-level testing API** allowing for the addition of per-contract or global post call/event property tests
-**Extensible high-level testing API** allowing for the addition of per-contract or global post call/event property tests with minimal effort.

## Installation

Expand Down
15 changes: 7 additions & 8 deletions chain/test_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ func (t *TestChain) GenesisDefinition() *core.Genesis {
return t.genesisDefinition
}

// State returns the current state.StateDB of the chain.
func (t *TestChain) State() *state.StateDB {
return t.state
}

// CommittedBlocks returns the real blocks which were committed to the chain, where methods such as BlockFromNumber
// return the simulated chain state with intermediate blocks injected for block number jumps, etc.
func (t *TestChain) CommittedBlocks() []*chainTypes.Block {
Expand Down Expand Up @@ -749,7 +754,7 @@ func (t *TestChain) PendingBlockDiscard() error {
}

// Emit our pending block discarded event
err = t.Events.PendingBlockCommitted.Publish(PendingBlockCommittedEvent{
err = t.Events.PendingBlockDiscarded.Publish(PendingBlockDiscardedEvent{
Chain: t,
Block: pendingBlock,
})
Expand Down Expand Up @@ -821,18 +826,12 @@ func (t *TestChain) emitContractChangeEvents(reverting bool, messageResults ...*
// test node, using the address provided as the deployer. Returns the address of the deployed contract if successful,
// the resulting block the deployment transaction was processed in, and an error if one occurred.
func (t *TestChain) DeployContract(contract *compilationTypes.CompiledContract, deployer common.Address) (common.Address, *chainTypes.Block, error) {
// Obtain the byte code as a byte array
b, err := contract.InitBytecodeBytes()
if err != nil {
return common.Address{}, nil, fmt.Errorf("could not convert compiled contract bytecode from hex string to byte code")
}

// Constructor args don't need ABI encoding and appending to the end of the bytecode since there are none for these
// contracts.

// Create a message to represent our contract deployment.
value := big.NewInt(0)
msg := t.CreateMessage(deployer, nil, value, nil, nil, b)
msg := t.CreateMessage(deployer, nil, value, nil, nil, contract.InitBytecode)

// Create a new pending block we'll commit to chain
block, err := t.PendingBlockCreate()
Expand Down
24 changes: 7 additions & 17 deletions chain/types/deployed_contract_bytecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,17 @@ type DeployedContractBytecode struct {
// IsMatch returns a boolean indicating whether the deployed contract bytecode is a match with the provided compiled
// contract.
func (c *DeployedContractBytecode) IsMatch(contract *types.CompiledContract) bool {
// Obtain the provided contract definition's init and runtime byte code/
contractInitBytecode, err := contract.InitBytecodeBytes()
if err != nil {
return false
}
contractRuntimeBytecode, err := contract.RuntimeBytecodeBytes()
if err != nil {
return false
}

// Check if we can compare init and runtime bytecode
canCompareInit := len(c.InitBytecode) > 0 && len(contractInitBytecode) > 0
canCompareRuntime := len(c.RuntimeBytecode) > 0 && len(contractRuntimeBytecode) > 0
canCompareInit := len(c.InitBytecode) > 0 && len(contract.InitBytecode) > 0
canCompareRuntime := len(c.RuntimeBytecode) > 0 && len(contract.RuntimeBytecode) > 0

// First try matching runtime bytecode contract metadata.
if canCompareRuntime {
// First we try to match contracts with contract metadata embedded within the smart contract.
// Note: We use runtime bytecode for this because init byte code can have matching metadata hashes for different
// contracts.
deploymentMetadata := types.ExtractContractMetadata(c.RuntimeBytecode)
definitionMetadata := types.ExtractContractMetadata(contractRuntimeBytecode)
definitionMetadata := types.ExtractContractMetadata(contract.RuntimeBytecode)
if deploymentMetadata != nil && definitionMetadata != nil {
deploymentBytecodeHash := deploymentMetadata.ExtractBytecodeHash()
definitionBytecodeHash := definitionMetadata.ExtractBytecodeHash()
Expand All @@ -80,16 +70,16 @@ func (c *DeployedContractBytecode) IsMatch(contract *types.CompiledContract) boo
// to match as a last ditch effort.
if canCompareInit {
// If the init byte code size is larger than what we initialized with, it is not a match.
if len(contractInitBytecode) > len(c.InitBytecode) {
if len(contract.InitBytecode) > len(c.InitBytecode) {
return false
}

// Cut down the contract init bytecode to the size of the definition's to attempt to strip away constructor
// arguments before performing a direct compare.
cutDeployedInitBytecode := c.InitBytecode[:len(contractInitBytecode)]
cutDeployedInitBytecode := c.InitBytecode[:len(contract.InitBytecode)]

// If the byte code matches exactly, we treat this as a match.
if bytes.Equal(cutDeployedInitBytecode, contractInitBytecode) {
if bytes.Equal(cutDeployedInitBytecode, contract.InitBytecode) {
return true
}
}
Expand All @@ -98,7 +88,7 @@ func (c *DeployedContractBytecode) IsMatch(contract *types.CompiledContract) boo
// process, e.g. smart contract constructor, will change the runtime code in most cases).
if canCompareRuntime {
// If the byte code matches exactly, we treat this as a match.
if bytes.Equal(c.RuntimeBytecode, contractRuntimeBytecode) {
if bytes.Equal(c.RuntimeBytecode, contract.RuntimeBytecode) {
return true
}
}
Expand Down
166 changes: 77 additions & 89 deletions compilation/platforms/crytic_compile.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package platforms

import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/trailofbits/medusa/compilation/types"
"github.com/trailofbits/medusa/utils"
Expand Down Expand Up @@ -55,13 +57,13 @@ func NewCryticCompilationConfig(target string) *CryticCompilationConfig {
}

// validateArgs ensures that the additional arguments provided to `crytic-compile` do not contain the `--export-format`
// or the `--export-dir` arguments. This is because `--export-format` has to be `standard` for the `crytic-compile`
// or the `--export-dir` arguments. This is because `--export-format` has to be `solc` for the `crytic-compile`
// integration to work and CryticCompilationConfig.BuildDirectory option is equivalent to `--export-dir`
func (c *CryticCompilationConfig) validateArgs() error {
// If --export-format or --export-dir are specified in c.Args, throw an error
for _, arg := range c.Args {
if arg == "--export-format" {
return errors.New("do not specify `--export-format` within crytic-compile arguments as the standard export format is always used")
return errors.New("do not specify `--export-format` within crytic-compile arguments as the solc export format is always used")
}
if arg == "--export-dir" {
return errors.New("do not specify `--export-dir` as an argument, use the BuildDirectory config variable instead")
Expand All @@ -72,8 +74,8 @@ func (c *CryticCompilationConfig) validateArgs() error {

// getArgs returns the arguments to be provided to crytic-compile during compilation, or an error if one occurs.
func (c *CryticCompilationConfig) getArgs() ([]string, error) {
// By default we export in solc-standard mode.
args := []string{c.Target, "--export-format", "standard"}
// By default we export in solc mode.
args := []string{c.Target, "--export-format", "solc"}

// Add --export-dir option if ExportDirectory is specified
if c.ExportDirectory != "" {
Expand All @@ -95,7 +97,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error)
}
err := utils.DeleteDirectory(exportDirectory)
if err != nil {
return nil, "", err
return nil, "", fmt.Errorf("could not delete crytic-compile's export directory prior to compilation, error: %v", err)
}

// Validate args to make sure --export-format and --export-dir are not specified
Expand Down Expand Up @@ -125,7 +127,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error)
}
}

// Run crytic-compile
// Run crytic-compile to compile and export our compilation artifacts.
out, err := cmd.CombinedOutput()
if err != nil {
return nil, "", fmt.Errorf("error while executing crytic-compile:\nOUTPUT:\n%s\nERROR: %s\n", string(out), err.Error())
Expand All @@ -137,112 +139,98 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error)
return nil, "", err
}

// Create a compilation list for a list of compilation units.
// Create a slice to track all our compilations parsed.
var compilationList []types.Compilation

// Define the structure of our crytic-compile export data.
type solcExportSource struct {
AST any `json:"AST"`
}
type solcExportContract struct {
SrcMap string `json:"srcmap"`
SrcMapRuntime string `json:"srcmap-runtime"`
Abi any `json:"abi"`
Bin string `json:"bin"`
BinRuntime string `json:"bin-runtime"`
}
type solcExportData struct {
Sources map[string]solcExportSource `json:"sources"`
Contracts map[string]solcExportContract `json:"contracts"`
}

// Loop through each .json file for compilation units.
for i := 0; i < len(matches); i++ {
// Read the compiled JSON file data
b, err := os.ReadFile(matches[i])
if err != nil {
return nil, "", err
return nil, "", fmt.Errorf("could not parse crytic-compile's exported solc data at path '%s', error: %v", matches[i], err)
}

// Parse the JSON
var compiledJson map[string]any
err = json.Unmarshal(b, &compiledJson)
var solcExport solcExportData
err = json.Unmarshal(b, &solcExport)
if err != nil {
return nil, "", err
return nil, "", fmt.Errorf("could not parse crytic-compile's exported solc data, error: %v", err)
}

// Index into "compilation_units" key
compilationUnits, ok := compiledJson["compilation_units"]
if !ok {
// If our json file does not have any compilation units, it is not a file of interest
continue
}
// Create a compilation object that will store the contracts and source information.
compilation := types.NewCompilation()

// Create a mapping between key (filename) and value (contract and ast information) each compilation unit
compilationMap, ok := compilationUnits.(map[string]any)
if !ok {
return nil, "", fmt.Errorf("compilationUnits is not in the map[string]any format: %s\n", compilationUnits)
// Loop through all sources and parse them into our types.
for sourcePath, source := range solcExport.Sources {
compilation.Sources[sourcePath] = types.CompiledSource{
Ast: source.AST,
Contracts: make(map[string]types.CompiledContract),
}
}

// Iterate through compilationUnits
for _, compilationUnit := range compilationMap {
// Create a compilation object that will store the contracts and asts for a single compilation unit
compilation := types.NewCompilation()

// Create mapping between key (compiler / asts / contracts) and associated values
compilationUnitMap, ok := compilationUnit.(map[string]any)
if !ok {
return nil, "", fmt.Errorf("compilationUnit is not in the map[string]any format: %s\n", compilationUnit)
// Loop through all contracts and parse them into our types.
for sourceAndContractPath, contract := range solcExport.Contracts {
// Split our source and contract path, as it takes the form sourcePath:contractName
splitIndex := strings.LastIndex(sourceAndContractPath, ":")
if splitIndex == -1 {
return nil, "", fmt.Errorf("expected contract path to be of form \"<source path>:<contract_name>\"")
}
sourcePath := sourceAndContractPath[:splitIndex]
contractName := sourceAndContractPath[splitIndex+1:]

// Ensure a source exists for this, or create one if our path somehow differed from any
// path not existing in the "sources" key at the root of the export.
if _, ok := compilation.Sources[sourcePath]; !ok {
parentSource := types.CompiledSource{
Ast: nil,
Contracts: make(map[string]types.CompiledContract),
}
compilation.Sources[sourcePath] = parentSource
}

// Create mapping between each file in compilation unit and associated Ast
AstMap := compilationUnitMap["asts"].(map[string]any)

// Create mapping between key (file name) and value (associated contracts in that file)
contractsMap, ok := compilationUnitMap["contracts"].(map[string]any)
if !ok {
return nil, "", fmt.Errorf("cannot find 'contracts' key in compilationUnitMap: %s\n", compilationUnitMap)
// Parse the ABI
contractAbi, err := types.ParseABIFromInterface(contract.Abi)
if err != nil {
return nil, "", fmt.Errorf("unable to parse ABI for contract '%s'\n", contractName)
}

// Iterate through each contract FILE (note that each FILE might have more than one contract)
for _, contractsData := range contractsMap {
// Create mapping between all contracts in a file (key) to it's data (abi, etc.)
contractMap, ok := contractsData.(map[string]any)
if !ok {
return nil, "", fmt.Errorf("contractsData is not in the map[string]any format: %s\n", contractsData)
}
// Decode our init and runtime bytecode
initBytecode, err := hex.DecodeString(strings.TrimPrefix(contract.Bin, "0x"))
if err != nil {
return nil, "", fmt.Errorf("unable to parse init bytecode for contract '%s'\n", contractName)
}
runtimeBytecode, err := hex.DecodeString(strings.TrimPrefix(contract.BinRuntime, "0x"))
if err != nil {
return nil, "", fmt.Errorf("unable to parse runtime bytecode for contract '%s'\n", contractName)
}

// Iterate through each contract
for contractName, contractData := range contractMap {
// Create mapping between contract details (abi, bytecode) to actual values
contractDataMap, ok := contractData.(map[string]any)
if !ok {
return nil, "", fmt.Errorf("contractData is not in the map[string]any format: %s\n", contractData)
}

// Create mapping between "filenames" (key) associated with the contract and the various filename
// types (absolute, relative, short, long)
fileMap, ok := contractDataMap["filenames"].(map[string]any)
if !ok {
return nil, "", fmt.Errorf("cannot find 'filenames' key in contractDataMap: %s\n", contractDataMap)
}

// Create unique source path which is going to be absolute path
sourcePath := fmt.Sprintf("%v", fileMap["absolute"])

// Parse the ABI
contractAbi, err := types.ParseABIFromInterface(contractDataMap["abi"])
if err != nil {
return nil, "", fmt.Errorf("Unable to parse ABI: %s\n", contractDataMap["abi"])
}

// Check if sourcePath has already been set (note that a sourcePath (i.e., file) can have more
// than one contract)
// sourcePath is also the key for the AstMap
if _, ok := compilation.Sources[sourcePath]; !ok {
compilation.Sources[sourcePath] = types.CompiledSource{
Ast: AstMap[sourcePath],
Contracts: make(map[string]types.CompiledContract),
}
}

// Add contract details
compilation.Sources[sourcePath].Contracts[contractName] = types.CompiledContract{
Abi: *contractAbi,
RuntimeBytecode: fmt.Sprintf("%v", contractDataMap["bin-runtime"]),
InitBytecode: fmt.Sprintf("%v", contractDataMap["bin"]),
SrcMapsInit: fmt.Sprintf("%v", contractDataMap["srcmap"]),
SrcMapsRuntime: fmt.Sprintf("%v", contractDataMap["srcmap-runtime"]),
}
}
// Add contract details
compilation.Sources[sourcePath].Contracts[contractName] = types.CompiledContract{
Abi: *contractAbi,
InitBytecode: initBytecode,
RuntimeBytecode: runtimeBytecode,
SrcMapsInit: contract.SrcMap,
SrcMapsRuntime: contract.SrcMapRuntime,
}
// Append compilation object to compilationList
compilationList = append(compilationList, *compilation)
}

compilationList = append(compilationList, *compilation)
}
// Return the compilationList
return compilationList, string(out), nil
Expand Down
15 changes: 13 additions & 2 deletions compilation/platforms/solc.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package platforms

import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -146,11 +147,21 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) {
continue
}

// Decode our init and runtime bytecode
initBytecode, err := hex.DecodeString(strings.TrimPrefix(contract.Code, "0x"))
if err != nil {
return nil, "", fmt.Errorf("unable to parse init bytecode for contract '%s'\n", contractName)
}
runtimeBytecode, err := hex.DecodeString(strings.TrimPrefix(contract.RuntimeCode, "0x"))
if err != nil {
return nil, "", fmt.Errorf("unable to parse runtime bytecode for contract '%s'\n", contractName)
}

// Construct our compiled contract
compilation.Sources[sourcePath].Contracts[contractName] = types.CompiledContract{
Abi: *contractAbi,
RuntimeBytecode: contract.RuntimeCode,
InitBytecode: contract.Code,
InitBytecode: initBytecode,
RuntimeBytecode: runtimeBytecode,
SrcMapsInit: contract.Info.SrcMap.(string),
SrcMapsRuntime: contract.Info.SrcMapRuntime,
}
Expand Down
Loading

0 comments on commit 612469e

Please sign in to comment.