Skip to content

add library support for crytic-compile platform #183

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ jobs:

- name: Install Python dependencies
run: |
pip3 install --no-cache-dir solc-select crytic-compile
pip3 install --no-cache-dir solc-select
pip3 install --no-cache-dir git+https://github.com/crytic/crytic-compile.git@feat/add-libraries-to-solc-export#egg=crytic-compile

- name: Install solc
run: |
Expand Down
26 changes: 10 additions & 16 deletions compilation/platforms/crytic_compile.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package platforms

import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -152,6 +151,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error)
Abi any `json:"abi"`
Bin string `json:"bin"`
BinRuntime string `json:"bin-runtime"`
LibraryDependencies []types.LinkInfo `json:"libraries"`
}
type solcExportData struct {
Sources map[string]solcExportSource `json:"sources"`
Expand Down Expand Up @@ -212,23 +212,17 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error)
return nil, "", fmt.Errorf("unable to parse ABI for contract '%s'\n", contractName)
}

// 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)
}

// Add contract details
// Add contract details. InitBytecode and RuntimeBytecode will be set by DecodeFullyLinkedBytecode after library linking
compilation.Sources[sourcePath].Contracts[contractName] = types.CompiledContract{
Abi: *contractAbi,
InitBytecode: initBytecode,
RuntimeBytecode: runtimeBytecode,
SrcMapsInit: contract.SrcMap,
SrcMapsRuntime: contract.SrcMapRuntime,
Abi: *contractAbi,
UnlinkedInitBytecode: contract.Bin,
InitBytecode: []byte{},
UnlinkedRuntimeBytecode: contract.BinRuntime,
RuntimeBytecode: []byte{},
SrcMapsInit: contract.SrcMap,
SrcMapsRuntime: contract.SrcMapRuntime,
LibraryDependencies: contract.LibraryDependencies,
}
}

Expand Down
37 changes: 34 additions & 3 deletions compilation/types/compiled_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,63 @@ package types

import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"golang.org/x/exp/slices"
"strings"
)

// LinkInfo provides the library name and placeholder string to replace in the unlinked bytecode.
type LinkInfo struct {
Name string
Placeholder string
}

// CompiledContract represents a single contract unit from a smart contract compilation.
type CompiledContract struct {
// Abi describes a contract's application binary interface, a structure used to describe information needed
// to interact with the contract such as constructor and function definitions with input/output variable
// information, event declarations, and fallback and receive methods.
Abi abi.ABI

// InitBytecode describes the bytecode used to deploy a contract.
// InitBytecode is the raw (potentially unlinked) init bytecode.
UnlinkedInitBytecode string

// InitBytecode is the hex-decoded bytecode used to deploy a contract.
InitBytecode []byte

// RuntimeBytecode represents the rudimentary bytecode to be expected once the contract has been successfully
// deployed. This may differ at runtime based on constructor arguments, immutables, linked libraries, etc.
// InitBytecode is the raw (potentially unlinked) runtime bytecode.
UnlinkedRuntimeBytecode string

// RuntimeBytecode is the bytecode expected to exist at the deployed address after construction.
// This may differ at runtime based on constructor arguments, immutables, etc.
RuntimeBytecode []byte

// SrcMapsInit describes the source mappings to associate source file and bytecode segments in InitBytecode.
SrcMapsInit string

// SrcMapsRuntime describes the source mappings to associate source file and bytecode segments in RuntimeBytecode.
SrcMapsRuntime string

// LibraryDependencies lists the resolved library dependencies for this contract in post-order.
// They must be deployed in this order to handle libraries which depend on other libraries.
LibraryDependencies []LinkInfo
}

// Decode our init and runtime bytecode from string to hex. This should be called after library linking.
func (c *CompiledContract) DecodeFullyLinkedBytecode() error {
var err error
c.InitBytecode, err = hex.DecodeString(strings.TrimPrefix(c.UnlinkedInitBytecode, "0x"))
if err != nil {
return fmt.Errorf("unable to parse init bytecode for contract. Are all libraries linked?\n")
}
c.RuntimeBytecode, err = hex.DecodeString(strings.TrimPrefix(c.UnlinkedRuntimeBytecode, "0x"))
if err != nil {
return fmt.Errorf("unable to parse runtime bytecode for contract\n Are all libraries linked?\n")
}
return nil
}

// IsMatch returns a boolean indicating whether provided contract bytecode is a match to this compiled contract
Expand Down
10 changes: 5 additions & 5 deletions fuzzing/contracts/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import (
"github.com/crytic/medusa/compilation/types"
)

// Contracts describes an array of contracts
type Contracts []*Contract
// Contracts describes a mapping of contracts by name
type Contracts map[string]*Contract

// MatchBytecode takes init and/or runtime bytecode and attempts to match it to a contract definition in the
// current list of contracts. It returns the contract definition if found. Otherwise, it returns nil.
func (c Contracts) MatchBytecode(initBytecode []byte, runtimeBytecode []byte) *Contract {
// Loop through all our contract definitions to find a match.
for i := 0; i < len(c); i++ {
for _, contract := range c {
// If we have a match, register the deployed contract.
if c[i].CompiledContract().IsMatch(initBytecode, runtimeBytecode) {
return c[i]
if contract.CompiledContract().IsMatch(initBytecode, runtimeBytecode) {
return contract
}
}

Expand Down
197 changes: 136 additions & 61 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"golang.org/x/exp/slices"
)

// Fuzzer represents an Ethereum smart contract fuzzing provider.
Expand Down Expand Up @@ -184,9 +183,13 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) {
return fuzzer, nil
}

// ContractDefinitions exposes the contract definitions registered with the Fuzzer.
// // ContractDefinitions exposes the contract definitions registered with the Fuzzer.
func (f *Fuzzer) ContractDefinitions() fuzzerTypes.Contracts {
return slices.Clone(f.contractDefinitions)
contractDefinitions := make(fuzzerTypes.Contracts)
for name, contract := range f.contractDefinitions {
contractDefinitions[name] = contract
}
return contractDefinitions
}

// Config exposes the underlying project configuration provided to the Fuzzer.
Expand Down Expand Up @@ -282,7 +285,7 @@ func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilati
for contractName := range source.Contracts {
contract := source.Contracts[contractName]
contractDefinition := fuzzerTypes.NewContract(contractName, sourcePath, &contract, compilation)
f.contractDefinitions = append(f.contractDefinitions, contractDefinition)
f.contractDefinitions[contractName] = contractDefinition
}
}

Expand Down Expand Up @@ -330,83 +333,155 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro
// we can infer the deployment order. Otherwise, we report an error.
if len(fuzzer.config.Fuzzing.DeploymentOrder) == 0 {
if len(fuzzer.contractDefinitions) == 1 {
fuzzer.config.Fuzzing.DeploymentOrder = []string{fuzzer.contractDefinitions[0].Name()}
for name, _ := range fuzzer.contractDefinitions {
fuzzer.config.Fuzzing.DeploymentOrder = []string{name}
}
} else {
return fmt.Errorf("you must specify a contract deployment order within your project configuration")
}
}

// Loop for all contracts to deploy
deployedContractAddr := make(map[string]common.Address)
libraryPlaceholders := make(map[string]string)

// Look for a contract in our compiled contract definitions that matches this one
for _, contractName := range fuzzer.config.Fuzzing.DeploymentOrder {
// Look for a contract in our compiled contract definitions that matches this one
found := false
for _, contract := range fuzzer.contractDefinitions {
// If we found a contract definition that matches this definition by name, try to deploy it
if contract.Name() == contractName {
args := make([]any, 0)
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)
}
decoded, err := valuegeneration.DecodeJSONArgumentsFromMap(contract.CompiledContract().Abi.Constructor.Inputs,
jsonArgs, deployedContractAddr)
if err != nil {
return err
}
args = decoded
}
if deployedContractAddr[contractName] != (common.Address{}) {
continue
}
contract, ok := fuzzer.contractDefinitions[contractName]

// Constructor 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)
}
if !ok {
return fmt.Errorf("DeploymentOrder specified a contract name which was not found in the compilation: %s", contractName)
}

// Create a message to represent our contract deployment (we let deployments consume the whole block
// gas limit rather than use tx gas limit)
msg := calls.NewCallMessage(fuzzer.deployer, nil, 0, big.NewInt(0), fuzzer.config.Fuzzing.BlockGasLimit, nil, nil, nil, msgData)
msg.FillFromTestChainProperties(testChain)
// Because the LibraryDependencies are in post-order, we do not need to determine the order in which to deploy
for _, linkInfo := range contract.CompiledContract().LibraryDependencies {
libraryName, libraryPlaceholder := linkInfo.Name, linkInfo.Placeholder

// Create a new pending block we'll commit to chain
block, err := testChain.PendingBlockCreate()
if err != nil {
return err
}
// If the library was already deployed (another contract/library depended on it), link to its existing address
if deployedContractAddr[libraryName] != (common.Address{}) {
addr := deployedContractAddr[libraryName].String()[2:]
contract.CompiledContract().UnlinkedInitBytecode = strings.ReplaceAll(contract.CompiledContract().UnlinkedInitBytecode, libraryPlaceholder, addr)
contract.CompiledContract().UnlinkedRuntimeBytecode = strings.ReplaceAll(contract.CompiledContract().UnlinkedRuntimeBytecode, libraryPlaceholder, addr)
continue
}

// Add our transaction to the block
err = testChain.PendingBlockAddTx(msg)
if err != nil {
return err
}
library, ok := fuzzer.contractDefinitions[libraryName]
if !ok {
return fmt.Errorf("library not found in compilation: %s", contractName)
}

// Commit the pending block to the chain, so it becomes the new head.
err = testChain.PendingBlockCommit()
if err != nil {
return err
}
// Link any previously deployed libraries that the current library depends on
for previousLibraryName, previousPlaceholder := range libraryPlaceholders {
previousDeployedAddress := deployedContractAddr[previousLibraryName].String()[2:]
library.CompiledContract().UnlinkedInitBytecode = strings.ReplaceAll(library.CompiledContract().UnlinkedInitBytecode, previousPlaceholder, previousDeployedAddress)
library.CompiledContract().UnlinkedRuntimeBytecode = strings.ReplaceAll(library.CompiledContract().UnlinkedRuntimeBytecode, previousPlaceholder, previousDeployedAddress)
}
// TODO create helper to deduplicate deployment code
// After linking, decode the bytecode so that init and runtime are set
err := library.CompiledContract().DecodeFullyLinkedBytecode()
if err != nil {
return err
}

// Ensure our transaction succeeded
if block.MessageResults[0].Receipt.Status != types.ReceiptStatusSuccessful {
return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err)
}
msg := calls.NewCallMessage(fuzzer.deployer, nil, 0, big.NewInt(0), fuzzer.config.Fuzzing.BlockGasLimit, nil, nil, nil, library.CompiledContract().InitBytecode)
msg.FillFromTestChainProperties(testChain)

// Create a new pending block we'll commit to chain
block, err := testChain.PendingBlockCreate()
if err != nil {
return err
}

// Add our transaction to the block
err = testChain.PendingBlockAddTx(msg)
if err != nil {
return err
}

// Commit the pending block to the chain, so it becomes the new head.
err = testChain.PendingBlockCommit()
if err != nil {
return err
}

// Ensure our transaction succeeded
if block.MessageResults[0].Receipt.Status != types.ReceiptStatusSuccessful {
return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err)
}

// Record our deployed contract so the next config-specified constructor args can reference this
// contract by name.
deployedAddress := block.MessageResults[0].Receipt.ContractAddress

deployedContractAddr[libraryName] = deployedAddress
libraryPlaceholders[libraryName] = libraryPlaceholder

// Record our deployed contract so the next config-specified constructor args can reference this
// contract by name.
deployedContractAddr[contractName] = block.MessageResults[0].Receipt.ContractAddress
// Link the library to the deployed address in the contract's bytecode
deployedAddressStr := deployedAddress.String()[2:]
contract.CompiledContract().UnlinkedInitBytecode = strings.ReplaceAll(contract.CompiledContract().UnlinkedInitBytecode, libraryPlaceholder, deployedAddressStr)
contract.CompiledContract().UnlinkedRuntimeBytecode = strings.ReplaceAll(contract.CompiledContract().UnlinkedRuntimeBytecode, libraryPlaceholder, deployedAddressStr)
}

// Flag that we found a matching compiled contract definition and deployed it, then exit out of this
// inner loop to process the next contract to deploy in the outer loop.
found = true
break
// After linking, decode the bytecode so that init and runtime are set
err := contract.CompiledContract().DecodeFullyLinkedBytecode()
if err != nil {
return err
}

args := make([]any, 0)
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)
}
decoded, err := valuegeneration.DecodeJSONArgumentsFromMap(contract.CompiledContract().Abi.Constructor.Inputs,
jsonArgs, deployedContractAddr)
if err != nil {
return err
}
args = decoded
}

// Constructor 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)
}

// If we did not find a contract corresponding to this item in the deployment order, we throw an error.
if !found {
return fmt.Errorf("DeploymentOrder specified a contract name which was not found in the compilation: %v\n", contractName)
// Create a message to represent our contract deployment (we let deployments consume the whole block
// gas limit rather than use tx gas limit)
msg := calls.NewCallMessage(fuzzer.deployer, nil, 0, big.NewInt(0), fuzzer.config.Fuzzing.BlockGasLimit, nil, nil, nil, msgData)
msg.FillFromTestChainProperties(testChain)

// Create a new pending block we'll commit to chain
block, err := testChain.PendingBlockCreate()
if err != nil {
return err
}

// Add our transaction to the block
err = testChain.PendingBlockAddTx(msg)
if err != nil {
return err
}

// Commit the pending block to the chain, so it becomes the new head.
err = testChain.PendingBlockCommit()
if err != nil {
return err
}

// Ensure our transaction succeeded
if block.MessageResults[0].Receipt.Status != types.ReceiptStatusSuccessful {
return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err)
}

// Record our deployed contract so the next config-specified constructor args can reference this
// contract by name.
deployedContractAddr[contractName] = block.MessageResults[0].Receipt.ContractAddress
}
return nil
}
Expand Down
Loading