From 197ccd1280151eaa3f149871755d9e096383e477 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 27 Jul 2023 23:53:40 -0500 Subject: [PATCH] add library support for crytic-compile platform --- .github/workflows/ci.yml | 3 +- compilation/platforms/crytic_compile.go | 26 +-- compilation/types/compiled_contract.go | 37 +++- fuzzing/contracts/contract.go | 10 +- fuzzing/fuzzer.go | 197 ++++++++++++------ fuzzing/fuzzer_test.go | 19 ++ .../deployments/external_library.sol | 27 +++ 7 files changed, 233 insertions(+), 86 deletions(-) create mode 100644 fuzzing/testdata/contracts/deployments/external_library.sol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3722c69c..0c772fbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | diff --git a/compilation/platforms/crytic_compile.go b/compilation/platforms/crytic_compile.go index 377d1a6d..6867b1ee 100644 --- a/compilation/platforms/crytic_compile.go +++ b/compilation/platforms/crytic_compile.go @@ -1,7 +1,6 @@ package platforms import ( - "encoding/hex" "encoding/json" "errors" "fmt" @@ -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"` @@ -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, } } diff --git a/compilation/types/compiled_contract.go b/compilation/types/compiled_contract.go index 37becf15..a202540f 100644 --- a/compilation/types/compiled_contract.go +++ b/compilation/types/compiled_contract.go @@ -2,6 +2,7 @@ package types import ( "bytes" + "encoding/hex" "encoding/json" "fmt" "github.com/ethereum/go-ethereum/accounts/abi" @@ -9,6 +10,12 @@ import ( "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 @@ -16,11 +23,17 @@ type CompiledContract struct { // 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. @@ -28,6 +41,24 @@ type CompiledContract struct { // 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 diff --git a/fuzzing/contracts/contract.go b/fuzzing/contracts/contract.go index 17ec9f66..ee3a0724 100644 --- a/fuzzing/contracts/contract.go +++ b/fuzzing/contracts/contract.go @@ -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 } } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index e64a7ba2..075458d3 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -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. @@ -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. @@ -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 } } @@ -330,7 +333,9 @@ 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") } @@ -338,75 +343,145 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro // 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 } diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 42ff13c5..4621f7c9 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -694,3 +694,22 @@ func TestDeploymentOrderWithCoverage(t *testing.T) { }, }) } + +// TestDeploymentsExternalLibrary runs a test to ensure external libraries behave correctly. +func TestDeploymentsExternalLibrary(t *testing.T) { + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: "testdata/contracts/deployments/external_library.sol", + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.DeploymentOrder = []string{"TestExternalLibrary"} + config.Fuzzing.TestLimit = 100 // this test should expose a failure quickly. + }, + method: func(f *fuzzerTestContext) { + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + // Check for any failed tests and verify coverage was captured + assertFailedTestsExpected(f, true) + assertCorpusCallSequencesCollected(f, true) + }, + }) +} diff --git a/fuzzing/testdata/contracts/deployments/external_library.sol b/fuzzing/testdata/contracts/deployments/external_library.sol new file mode 100644 index 00000000..7dedc9fa --- /dev/null +++ b/fuzzing/testdata/contracts/deployments/external_library.sol @@ -0,0 +1,27 @@ +library Library1 { + function getLibrary() public returns(string memory) { + return "Library"; + } +} +library Library2 { + function getLibrary() public returns(string memory) { + Library1.getLibrary(); + } +} +library Library3 { + function getLibrary() public returns(string memory) { + Library2.getLibrary(); + Library1.getLibrary(); + } +} +contract TestExternalLibrary { + function test() public { + Library3.getLibrary(); + + } + function fuzz_me() public returns(bool){ + if (keccak256(abi.encodePacked(Library3.getLibrary())) == keccak256(abi.encodePacked("Library"))) { + return false; + } + } +} \ No newline at end of file