From 3cef3de622a3e6894d7244028edca5d7eb39be70 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 5 Feb 2025 14:58:59 -0500 Subject: [PATCH] Fix issue related to chain cloning (#564) * preliminary commit * optimization bug fix * add base block context * fix cheatcode bugs, add prevrandao, and deprecate difficulty * revert version number * fix optimization mode bug * update solc version in CI * update solc version again * fix context management * fix context bug and improve logging while shrinking * enable fork mode when --rpc-url is specified * update documentation * last update * i genuinely hate prettier * update docs --- .github/workflows/ci.yml | 2 +- chain/block_context.go | 3 +- chain/standard_cheat_code_contract.go | 26 +++++--- chain/test_chain.go | 61 ++++++++++++------- chain/types/base_block_context.go | 33 ++++++++++ chain/types/block.go | 16 ++++- cmd/fuzz_flags.go | 7 ++- cmd/root.go | 3 +- compilation/types/slither.go | 2 +- docs/src/SUMMARY.md | 2 + docs/src/cheatcodes/cheatcodes_overview.md | 5 +- docs/src/cheatcodes/difficulty.md | 17 +----- docs/src/cheatcodes/prevrandao.md | 22 +++++++ .../src/project_configuration/chain_config.md | 11 ++-- .../compilation_config.md | 1 + .../project_configuration/fuzzing_config.md | 10 ++- .../project_configuration/slither_config.md | 10 +++ docs/src/static/medusa.json | 14 ++++- fuzzing/fuzzer.go | 7 ++- fuzzing/fuzzer_hooks.go | 3 + fuzzing/fuzzer_test.go | 4 +- fuzzing/fuzzer_worker.go | 12 +++- fuzzing/test_case_assertion_provider.go | 1 + fuzzing/test_case_optimization.go | 34 +++++++++-- fuzzing/test_case_optimization_provider.go | 18 ++---- fuzzing/test_case_property_provider.go | 1 + .../contracts/cheat_codes/vm/difficulty.sol | 10 +-- .../cheat_codes/vm/fee_permanent.sol | 3 - .../contracts/cheat_codes/vm/prevrandao.sol | 17 ++++++ .../cheat_codes/vm/roll_permanent.sol | 10 +-- .../cheat_codes/vm/warp_permanent.sol | 10 +-- .../valuegeneration/generator_mutational.go | 8 +-- 32 files changed, 278 insertions(+), 105 deletions(-) create mode 100644 chain/types/base_block_context.go create mode 100644 docs/src/cheatcodes/prevrandao.md create mode 100644 fuzzing/testdata/contracts/cheat_codes/vm/prevrandao.sol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88b63890..66faa6a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: - name: Install solc run: | - solc-select use 0.8.17 --always-install + solc-select use 0.8.28 --always-install - name: Test run: go test ./... diff --git a/chain/block_context.go b/chain/block_context.go index c145e897..f6255339 100644 --- a/chain/block_context.go +++ b/chain/block_context.go @@ -1,11 +1,12 @@ package chain import ( + "math/big" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "math/big" ) // newTestChainBlockContext obtains a new vm.BlockContext that is tailored to provide data from a TestChain. diff --git a/chain/standard_cheat_code_contract.go b/chain/standard_cheat_code_contract.go index 81f0fe6d..a6274f06 100644 --- a/chain/standard_cheat_code_contract.go +++ b/chain/standard_cheat_code_contract.go @@ -172,18 +172,28 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, }, ) - // Difficulty: Updates difficulty - // TODO: Make changes to difficulty permanent and make it revert for post-Paris EVM versions + // Difficulty: Updates difficulty. Since we do not allow users to choose the fork that + // they are using (for now), and we are using a post-Paris fork, the difficulty cheatcode is a no-op. contract.addMethod( "difficulty", abi.Arguments{{Type: typeUint256}}, abi.Arguments{}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { - // Maintain our changes until the transaction exits. - spoofedDifficulty := inputs[0].(*big.Int) - spoofedDifficultyHash := common.BigToHash(spoofedDifficulty) + return nil, nil + }, + ) + + // Prevrandao: Updates random. + contract.addMethod( + "prevrandao", abi.Arguments{{Type: typeBytes32}}, abi.Arguments{}, + func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { + // Store our original random originalRandom := tracer.chain.pendingBlockContext.Random - // In newer evm versions, block.difficulty uses opRandom instead of opDifficulty. - tracer.chain.pendingBlockContext.Random = &spoofedDifficultyHash + // Update the pending block context random + newRandom := inputs[0].([32]byte) + newRandomHash := common.BytesToHash(newRandom[:]) + tracer.chain.pendingBlockContext.Random = &newRandomHash + + // Restore the original random when top frame exits tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() { tracer.chain.pendingBlockContext.Random = originalRandom }) @@ -191,8 +201,6 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, }, ) - // TODO: Add prevrandao cheatcode - // Coinbase: Updates the block coinbase. Note that this _permanently_ updates the coinbase for the remainder of the // chain's lifecycle contract.addMethod( diff --git a/chain/test_chain.go b/chain/test_chain.go index 2f2a1e38..35df71db 100644 --- a/chain/test_chain.go +++ b/chain/test_chain.go @@ -3,9 +3,10 @@ package chain import ( "errors" "fmt" + "math/big" + "github.com/crytic/medusa/chain/state" "golang.org/x/net/context" - "math/big" "github.com/crytic/medusa/chain/config" "github.com/ethereum/go-ethereum/core/rawdb" @@ -211,7 +212,6 @@ func newTestChainWithStateFactory( // Convert our genesis block (go-ethereum type) to a test chain block. testChainGenesisBlock := types.NewBlock(genesisBlock.Header()) - // Create our state database over-top our database. stateDatabase := gethState.NewDatabaseWithConfig(db, dbConfig) @@ -288,8 +288,9 @@ func (t *TestChain) Clone(onCreateFunc func(chain *TestChain) error) (*TestChain // did originally. for i := 1; i < len(t.blocks); i++ { // First create a new pending block to commit - blockHeader := t.blocks[i].Header - _, err = targetChain.PendingBlockCreateWithParameters(blockHeader.Number.Uint64(), blockHeader.Time, &blockHeader.GasLimit) + block := t.blocks[i] + blockHeader := block.Header + _, err = targetChain.PendingBlockCreateWithBaseBlockContext(block.BaseContext, &blockHeader.GasLimit) if err != nil { return nil, err } @@ -578,11 +579,10 @@ func (t *TestChain) PendingBlockCreate() (*types.Block, error) { return t.PendingBlockCreateWithParameters(blockNumber, timestamp, nil) } -// PendingBlockCreateWithParameters constructs an empty block which is pending addition to the chain, using the block -// properties provided. Note that there are no constraints on the next block number or timestamp. Because of cheatcode -// usage, the next block can go back in time. -// Returns the constructed block, or an error if one occurred. -func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTime uint64, blockGasLimit *uint64) (*types.Block, error) { +// PendingBlockCreateWithBaseBlockContext constructs an empty block which is pending addition to the chain, using the +// provided base block context. The base block context holds information such as the block number, timestamp, and base fee +// that should be used to initialize the block. +func (t *TestChain) PendingBlockCreateWithBaseBlockContext(baseBlockContext *types.BaseBlockContext, blockGasLimit *uint64) (*types.Block, error) { // If we already have a pending block, return an error. if t.pendingBlock != nil { return nil, fmt.Errorf("could not create a new pending block for chain, as a block is already pending") @@ -593,41 +593,43 @@ func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTi blockGasLimit = &t.BlockGasLimit } - // Obtain our parent block hash to reference in our new block. - parentBlockHash := t.Head().Hash - // Note we do not perform any block number or timestamp validation since cheatcodes can permanently update the // block number or timestamp which could violate the invariants of a blockchain (e.g. block.number is strictly // increasing) + // Obtain our parent block hash to reference in our new block. + parentBlockHash := t.Head().Hash + // Create a block header for this block: // - State root hash reflects the state after applying block updates (no transactions, so unchanged from last block) + // - Other hashes will populate as we apply transactions // - Bloom is aggregated for each transaction in the block (for now empty). - // - TODO: Difficulty should be revisited/checked. // - GasUsed is aggregated for each transaction in the block (for now zero). - // - Mix digest is only useful for randomness, so we just fake randomness by using the previous block hash. - // - TODO: BaseFee should be revisited/checked. + // - We don't care too much about difficulty and mix digest so setting them to random things header := &gethTypes.Header{ ParentHash: parentBlockHash, UncleHash: gethTypes.EmptyUncleHash, - Coinbase: t.Head().Header.Coinbase, Root: t.Head().Header.Root, TxHash: gethTypes.EmptyRootHash, ReceiptHash: gethTypes.EmptyRootHash, Bloom: gethTypes.Bloom{}, - Difficulty: common.Big0, - Number: big.NewInt(int64(blockNumber)), GasLimit: *blockGasLimit, GasUsed: 0, - Time: blockTime, Extra: []byte{}, - MixDigest: parentBlockHash, Nonce: gethTypes.BlockNonce{}, - BaseFee: new(big.Int).Set(t.Head().Header.BaseFee), + Coinbase: baseBlockContext.Coinbase, + Difficulty: common.Big0, + Number: new(big.Int).Set(baseBlockContext.Number), + Time: baseBlockContext.Time, + MixDigest: parentBlockHash, + BaseFee: new(big.Int).Set(baseBlockContext.BaseFee), } - // Create a new block for our test node + // Create a new block for our test chain t.pendingBlock = types.NewBlock(header) + + // Set the block hash + // Note that this block hash may change if cheatcodes that update the block header are used (e.g. warp) t.pendingBlock.Hash = t.pendingBlock.Header.Hash() // Emit our event for the pending block being created @@ -643,6 +645,21 @@ func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTi return t.pendingBlock, nil } +// PendingBlockCreateWithParameters constructs an empty block which is pending addition to the chain, using the block number +// and timestamp provided. Returns the constructed block, or an error if one occurred. +func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTime uint64, blockGasLimit *uint64) (*types.Block, error) { + // We will create a base block context with the provided parameters in addition to using the current head block. + // All values that are not the block number and timestamp are taken from the current head block. + baseBlockContext := types.NewBaseBlockContext( + blockNumber, + blockTime, + t.Head().Header.BaseFee, + t.Head().Header.Coinbase, + ) + + return t.PendingBlockCreateWithBaseBlockContext(baseBlockContext, blockGasLimit) +} + // PendingBlockAddTx takes a message (internal txs) and adds it to the current pending block, updating the header // with relevant execution information. If a pending block was not created, an error is returned. // Returns an error if one occurred. diff --git a/chain/types/base_block_context.go b/chain/types/base_block_context.go new file mode 100644 index 00000000..0eae1574 --- /dev/null +++ b/chain/types/base_block_context.go @@ -0,0 +1,33 @@ +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// BaseBlockContext stores block-level information (e.g. block.number or block.timestamp) when the block is first +// created. We need to store these values because cheatcodes like warp or roll will directly modify the block header. +// We use these values during the cloning process to ensure that execution semantics are maintained while still +// allowing the cheatcodes to function as expected. We could expand this struct to hold additional values +// (e.g. difficulty) but we will ere to add values only as necessary. +type BaseBlockContext struct { + // Number represents the block number of the block when it was first created. + Number *big.Int + // Time represents the timestamp of the block when it was first created. + Time uint64 + // BaseFee represents the base fee of the block when it was first created. + BaseFee *big.Int + // Coinbase represents the coinbase of the block when it was first created. + Coinbase common.Address +} + +// NewBaseBlockContext returns a new BaseBlockContext with the provided parameters. +func NewBaseBlockContext(number uint64, time uint64, baseFee *big.Int, coinbase common.Address) *BaseBlockContext { + return &BaseBlockContext{ + Number: new(big.Int).SetUint64(number), + Time: time, + BaseFee: new(big.Int).Set(baseFee), + Coinbase: coinbase, + } +} diff --git a/chain/types/block.go b/chain/types/block.go index 754f81ff..53b821d6 100644 --- a/chain/types/block.go +++ b/chain/types/block.go @@ -8,10 +8,10 @@ import ( // Block represents a rudimentary block structure generated by sending messages to a test chain. type Block struct { - // hash represents the block hash for this block. + // Hash represents the block hash for this block. Hash common.Hash - // header represents the block header for this current block. + // Header represents the block header for this current block. Header *types.Header // Messages represent internal EVM core.Message objects. Messages are derived from transactions after validation @@ -22,6 +22,12 @@ type Block struct { // MessageResults represents the results recorded while executing transactions. MessageResults []*MessageResults + + // BaseContext stores the initial (base) block context before the execution of any transactions + // within the block. Since transactions that use cheatcodes can affect the block header + // permanently, we need to store the original values so that we can maintain execution + // semantics and allow for the chain to be clone-able. + BaseContext *BaseBlockContext } // NewBlock returns a new Block with the provided parameters. @@ -32,6 +38,12 @@ func NewBlock(header *types.Header) *Block { Header: header, Messages: make([]*core.Message, 0), MessageResults: make([]*MessageResults, 0), + BaseContext: NewBaseBlockContext( + header.Number.Uint64(), + header.Time, + header.BaseFee, + header.Coinbase, + ), } return block } diff --git a/cmd/fuzz_flags.go b/cmd/fuzz_flags.go index 637d1935..07bdb89a 100644 --- a/cmd/fuzz_flags.go +++ b/cmd/fuzz_flags.go @@ -94,7 +94,6 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. if err != nil { return err } - err = projectConfig.Compilation.SetTarget(newTarget) if err != nil { return err @@ -226,10 +225,14 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. // Update RPC url if cmd.Flags().Changed("rpc-url") { - projectConfig.Fuzzing.TestChainConfig.ForkConfig.RpcUrl, err = cmd.Flags().GetString("rpc-url") + rpcUrl, err := cmd.Flags().GetString("rpc-url") if err != nil { return err } + + // Enable on-chain fuzzing with the given URL + projectConfig.Fuzzing.TestChainConfig.ForkConfig.ForkModeEnabled = true + projectConfig.Fuzzing.TestChainConfig.ForkConfig.RpcUrl = rpcUrl } // Update RPC block diff --git a/cmd/root.go b/cmd/root.go index e333d155..6d1e3159 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,10 +1,11 @@ package cmd import ( + "os" + "github.com/crytic/medusa/logging" "github.com/rs/zerolog" "github.com/spf13/cobra" - "os" ) const version = "1.0.0" diff --git a/compilation/types/slither.go b/compilation/types/slither.go index f0d0b3f7..76aab0b1 100644 --- a/compilation/types/slither.go +++ b/compilation/types/slither.go @@ -70,7 +70,7 @@ func (s *SlitherConfig) validateArgs() error { // getArgs returns the arguments to be provided to slither, or an error if one occurs. // The slither target is provided as an input argument. func (s *SlitherConfig) getArgs(target string) ([]string, error) { - // By default we do not re-compile, use the echidna printer, and output in json format + // By default, we do not re-compile, use the echidna printer, and output in json format args := []string{target, "--ignore-compile", "--print", "echidna", "--json", "-"} // Add remaining args diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 383a71ef..2542120a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -14,6 +14,7 @@ - [Testing Configuration](project_configuration/testing_config.md) - [Chain Configuration](project_configuration/chain_config.md) - [Compilation Configuration](project_configuration/compilation_config.md) +- [Slither Configuration](project_configuration/slither_config.md) - [Logging Configuration](project_configuration/logging_config.md) # Command Line Interface (CLI) @@ -43,6 +44,7 @@ - [roll](./cheatcodes/roll.md) - [fee](./cheatcodes/fee.md) - [difficulty](./cheatcodes/difficulty.md) + - [prevrandao](./cheatcodes/prevrandao.md) - [chainId](./cheatcodes/chain_id.md) - [store](./cheatcodes/store.md) - [load](./cheatcodes/load.md) diff --git a/docs/src/cheatcodes/cheatcodes_overview.md b/docs/src/cheatcodes/cheatcodes_overview.md index ec2dcf46..978d6ed7 100644 --- a/docs/src/cheatcodes/cheatcodes_overview.md +++ b/docs/src/cheatcodes/cheatcodes_overview.md @@ -21,9 +21,12 @@ interface StdCheats { // Set block.basefee function fee(uint256) external; - // Set block.difficulty and block.prevrandao + // Set block.difficulty (deprecated in `medusa`) function difficulty(uint256) external; + // Set block.prevrandao + function prevrandao(bytes32) external; + // Set block.chainid function chainId(uint256) external; diff --git a/docs/src/cheatcodes/difficulty.md b/docs/src/cheatcodes/difficulty.md index fa901849..4727c6e0 100644 --- a/docs/src/cheatcodes/difficulty.md +++ b/docs/src/cheatcodes/difficulty.md @@ -2,21 +2,8 @@ ## Description -The `difficulty` cheatcode will set the `block.difficulty` and the `block.prevrandao` value. At the moment, both values -are changed since the cheatcode does not check what EVM version is running. - -Note that this behavior will change in the future. - -## Example - -```solidity -// Obtain our cheat code contract reference. -IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - -// Change value and verify. -cheats.difficulty(x); -assert(block.difficulty == x); -``` +The `difficulty` cheatcode has been deprecated in `medusa`. Since `medusa` uses a post-Paris EVM version, the cheatcode +will not update the `block.difficulty` and instead calling it will be a no-op. ## Function Signature diff --git a/docs/src/cheatcodes/prevrandao.md b/docs/src/cheatcodes/prevrandao.md new file mode 100644 index 00000000..7707d031 --- /dev/null +++ b/docs/src/cheatcodes/prevrandao.md @@ -0,0 +1,22 @@ +# `prevrandao` + +## Description + +The `prevrandao` cheatcode updates the `block.prevrandao`. + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Change value and verify. +cheats.prevrandao(bytes32(uint256(42))); +assert(block.prevrandao == 42); +``` + +## Function Signature + +```solidity +function prevrandao(bytes32) external; +``` diff --git a/docs/src/project_configuration/chain_config.md b/docs/src/project_configuration/chain_config.md index efccd71b..0fbaef51 100644 --- a/docs/src/project_configuration/chain_config.md +++ b/docs/src/project_configuration/chain_config.md @@ -6,13 +6,14 @@ The chain configuration defines the parameters for setting up `medusa`'s underly - **Type**: Boolean - **Description**: If `true`, the maximum code size check of 24576 bytes in `go-ethereum` is disabled. -- > 🚩 Setting `codeSizeCheckDisabled` to `false` is not recommended since it complicates the fuzz testing process. + > 🚩 Setting `codeSizeCheckDisabled` to `false` is not recommended since it complicates the fuzz testing process. - **Default**: `true` ### `skipAccountChecks` - **Type**: Boolean - **Description**: If `true`, account-related checks (nonce validation, transaction origin must be an EOA) are disabled in `go-ethereum`. + > 🚩 Setting `codeSizeCheckDisabled` to `false` is not recommended since it complicates the fuzz testing process. - **Default**: `true` ## Cheatcode Configuration @@ -42,16 +43,18 @@ The chain configuration defines the parameters for setting up `medusa`'s underly - **Type**: String - **Description**: Determines the RPC URL that will be queried during fork mode. -- **Default**: `n/a` +- **Default**: "" ### `rpcBlock` - **Type**: Integer -- **Description**: Determines the block height that fork state will be queried for. Block tags like `LATEST` are not supported. +- **Description**: Determines the block height that fork state will be queried for. Block tags like `LATEST` are not supported +- yet. - **Default**: `1` ### `poolSize` - **Type**: Integer -- **Description**: Determines the size of the client pool used to query the RPC. It is recommended to use a pool size that is 2-3x the number of workers used, but smaller pools may be required to avoid exceeding external RPC query limits. +- **Description**: Determines the size of the client pool used to query the RPC. It is recommended to use a pool size +- that is 2-3x the number of workers used, but smaller pools may be required to avoid exceeding external RPC query limits. - **Default**: `20` diff --git a/docs/src/project_configuration/compilation_config.md b/docs/src/project_configuration/compilation_config.md index 4e298fdf..5c4eac41 100644 --- a/docs/src/project_configuration/compilation_config.md +++ b/docs/src/project_configuration/compilation_config.md @@ -50,6 +50,7 @@ The compilation configuration defines the parameters to use while compiling a ta `args` value will be `"args": ["--compile-force-framework", "foundry"]`. > 🚩 The `--export-format` and `--export-dir` are already used during compilation with `crytic-compile`. > Re-using these flags in `args` will cause the compilation to fail. +- **Default**: `[]` ### `platformConfig` for `solc` diff --git a/docs/src/project_configuration/fuzzing_config.md b/docs/src/project_configuration/fuzzing_config.md index 5adcd067..d48a70e6 100644 --- a/docs/src/project_configuration/fuzzing_config.md +++ b/docs/src/project_configuration/fuzzing_config.md @@ -33,6 +33,13 @@ The fuzzing configuration defines the parameters for the fuzzing campaign. is provided, no test limit will be enforced. - **Default**: 0 calls +### `shrinkLimit` + +- **Type**: Integer +- **Description**: The number of iterations that shrinking will run for before returning the shrunken call sequence. +- **Default**: 5000 iterations +- + ### `callSequenceLength` - **Type**: Integer @@ -52,7 +59,8 @@ The fuzzing configuration defines the parameters for the fuzzing campaign. - **Type**: String - **Description**: The file path where the corpus should be saved. The corpus collects sequences during a fuzzing campaign that help drive fuzzer features (e.g. a call sequence that increases code coverage is stored in the corpus). These sequences - can then be re-used/mutated by the fuzzer during the next fuzzing campaign. + can then be re-used/mutated by the fuzzer during the next fuzzing campaign. Note that if the `corpusDirectory` is + left as an empty string (which it is by default), no corpus will be loaded from disk and stored to disk. - **Default**: "" ### `coverageFormats` diff --git a/docs/src/project_configuration/slither_config.md b/docs/src/project_configuration/slither_config.md index b7959d3f..a1329295 100644 --- a/docs/src/project_configuration/slither_config.md +++ b/docs/src/project_configuration/slither_config.md @@ -25,3 +25,13 @@ best to mine constants from each contract's AST so don't worry! is computationally intensive for complex projects. We recommend disabling caching (by making `cachePath` an empty string) if the target codebase changes. If the code remains constant during the fuzzing campaign, we recommend to use the cache. - **Default**: `slither_results.json` + +### `args` + +- **Type**: [String] +- **Description**: Refers to any additional args that one may want to provide to Slither. Run `slither --help` + to view all of its supported flags. For example, if you would like to specify `--foundry-out-directory out`, the + `args` value will be `"args": ["--foundry-out-directory", "out"]`. + > 🚩 The `--ignore-compile`, `--print`, and `--json` are already used during compilation with Slither. + > Re-using these flags in `args` will cause the compilation to fail. +- **Default**: `[]` diff --git a/docs/src/static/medusa.json b/docs/src/static/medusa.json index 8d08a8d0..603cb8a8 100644 --- a/docs/src/static/medusa.json +++ b/docs/src/static/medusa.json @@ -8,6 +8,7 @@ "callSequenceLength": 100, "corpusDirectory": "", "coverageEnabled": true, + "coverageFormats": ["html", "lcov"], "targetContracts": [], "predeployedContracts": {}, "targetContractsBalances": [], @@ -57,7 +58,13 @@ "cheatCodesEnabled": true, "enableFFI": false }, - "skipAccountChecks": true + "skipAccountChecks": true, + "forkConfig": { + "forkModeEnabled": false, + "rpcUrl": "", + "rpcBlock": 1, + "poolSize": 20 + } } }, "compilation": { @@ -69,6 +76,11 @@ "args": [] } }, + "slither": { + "useSlither": true, + "cachePath": "slither_results.json", + "args": [] + }, "logging": { "level": "info", "logDirectory": "", diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index b96345ee..57afd149 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -660,7 +660,7 @@ func (f *Fuzzer) spawnWorkersLoop(baseTestChain *chain.TestChain) error { } // Define a flag that indicates whether we have cancelled fuzzing or not - working := !(utils.CheckContextDone(f.ctx) || utils.CheckContextDone(f.emergencyCtx)) + working := !utils.CheckContextDone(f.ctx) // Create workers and start fuzzing. var err error @@ -930,6 +930,11 @@ func (f *Fuzzer) Terminate() { if f.emergencyCtxCancelFunc != nil { f.emergencyCtxCancelFunc() } + + // Cancel the main context as well + if f.ctxCancelFunc != nil { + f.ctxCancelFunc() + } } // printMetricsLoop prints metrics to the console in a loop until ctx signals a stopped operation. diff --git a/fuzzing/fuzzer_hooks.go b/fuzzing/fuzzer_hooks.go index 2ec49fdf..96b15f93 100644 --- a/fuzzing/fuzzer_hooks.go +++ b/fuzzing/fuzzer_hooks.go @@ -57,6 +57,9 @@ type CallSequenceTestFunc func(worker *FuzzerWorker, callSequence calls.CallSequ // ShrinkCallSequenceRequest is a structure signifying a request for a shrunken call sequence from the FuzzerWorker. type ShrinkCallSequenceRequest struct { + // TestName represents the name of the test case that is having a call sequence that is being shrunk. + // It is primarily used for logging. + TestName string // CallSequenceToShrink represents the _original_ CallSequence that needs to be shrunk CallSequenceToShrink calls.CallSequence // VerifierFunction is a method is called upon by a FuzzerWorker to check if a shrunken call sequence satisfies diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index c0674da1..ba0b3b57 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -8,11 +8,10 @@ import ( "reflect" "testing" - "github.com/crytic/medusa/fuzzing/executiontracer" - "github.com/crytic/medusa/chain" "github.com/crytic/medusa/events" "github.com/crytic/medusa/fuzzing/calls" + "github.com/crytic/medusa/fuzzing/executiontracer" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/ethereum/go-ethereum/common" @@ -280,6 +279,7 @@ func TestCheatCodes(t *testing.T) { "testdata/contracts/cheat_codes/vm/store_load.sol", "testdata/contracts/cheat_codes/vm/warp.sol", "testdata/contracts/cheat_codes/vm/warp_permanent.sol", + "testdata/contracts/cheat_codes/vm/prevrandao.sol", } // FFI test will fail on Windows because "echo" is a shell command, not a system command, so we diverge these diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 4015a9e4..fc97c2ec 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -2,6 +2,7 @@ package fuzzing import ( "fmt" + "github.com/crytic/medusa/logging/colors" "math/big" "math/rand" @@ -328,7 +329,7 @@ func (fw *FuzzerWorker) testNextCallSequence() ([]ShrinkCallSequenceRequest, err fw.workerMetrics().gasUsed.Add(fw.workerMetrics().gasUsed, new(big.Int).SetUint64(lastCallSequenceElement.ChainReference.Block.MessageResults[lastCallSequenceElement.ChainReference.TransactionIndex].Receipt.GasUsed)) // If our fuzzer context or the emergency context is cancelled, exit out immediately without results. - if utils.CheckContextDone(fw.fuzzer.ctx) || utils.CheckContextDone(fw.fuzzer.emergencyCtx) { + if utils.CheckContextDone(fw.fuzzer.ctx) { return true, nil } @@ -345,7 +346,7 @@ func (fw *FuzzerWorker) testNextCallSequence() ([]ShrinkCallSequenceRequest, err } // If our fuzzer context is done, exit out immediately without results. - if utils.CheckContextDone(fw.fuzzer.ctx) || utils.CheckContextDone(fw.fuzzer.emergencyCtx) { + if utils.CheckContextDone(fw.fuzzer.ctx) { return nil, nil } @@ -454,7 +455,8 @@ func (fw *FuzzerWorker) shrinkCallSequence(shrinkRequest ShrinkCallSequenceReque // 2) Add block/time delay to previous call (retain original block/time, possibly exceed max delays) // At worst, this costs `2 * len(callSequence)` shrink iterations. fw.workerMetrics().shrinking = true - fw.fuzzer.logger.Info(fmt.Sprintf("[Worker %d] Shrinking call sequence with %d call(s)", fw.workerIndex, len(shrinkRequest.CallSequenceToShrink))) + fw.fuzzer.logger.Info("[Worker ", fw.workerIndex, "] Shrinking call sequence for ", colors.GreenBold, + shrinkRequest.TestName, colors.Bold, " with ", len(shrinkRequest.CallSequenceToShrink), " call(s)") for removalStrategy := 0; removalStrategy < 2 && !shrinkingEnded(); removalStrategy++ { for i := len(optimizedSequence) - 1; i >= 0 && !shrinkingEnded(); i-- { @@ -644,6 +646,10 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { // Run all shrink requests for _, shrinkCallSequenceRequest := range fw.shrinkCallSequenceRequests { + // Immediately exit if we are shutting down + if utils.CheckContextDone(fw.fuzzer.emergencyCtx) { + return true, nil + } _, err = fw.shrinkCallSequence(shrinkCallSequenceRequest) if err != nil { return false, err diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index 90405f7e..1c2fb86f 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -191,6 +191,7 @@ func (t *AssertionTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorke if testFailed { // Create a request to shrink this call sequence. shrinkRequest := ShrinkCallSequenceRequest{ + TestName: testCase.Name(), CallSequenceToShrink: callSequence, VerifierFunction: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) (bool, error) { // Obtain the method ID for the last call and check if it encountered assertion failures. diff --git a/fuzzing/test_case_optimization.go b/fuzzing/test_case_optimization.go index 61ec7b38..be8f7374 100644 --- a/fuzzing/test_case_optimization.go +++ b/fuzzing/test_case_optimization.go @@ -52,14 +52,36 @@ func (t *OptimizationTestCase) Name() string { func (t *OptimizationTestCase) LogMessage() *logging.LogBuffer { buffer := logging.NewLogBuffer() - // Note that optimization tests will always pass + // If the test case never started, just log the status and name of the test case + if t.Status() == TestCaseStatusNotStarted { + buffer.Append(colors.GreenBold, fmt.Sprintf("[%s] ", t.Status()), colors.Bold, t.Name(), colors.Reset, "\n") + return buffer + } + + // We are now guaranteed to handle only test cases in the running state + // If we weren't able to find a value greater than the minimum, the test case has failed + minInt, _ := new(big.Int).SetString(MIN_INT, 16) + if t.Value().Cmp(minInt) == 0 { + t.status = TestCaseStatusFailed + } else { + t.status = TestCaseStatusPassed + } buffer.Append(colors.GreenBold, fmt.Sprintf("[%s] ", t.Status()), colors.Bold, t.Name(), colors.Reset, "\n") - if t.Status() != TestCaseStatusNotStarted { - buffer.Append(fmt.Sprintf("Test for method \"%s.%s\" resulted in the maximum value: ", t.targetContract.Name(), t.targetMethod.Sig)) - buffer.Append(colors.Bold, t.value, colors.Reset, "\n") - buffer.Append(colors.Bold, "[Call Sequence]", colors.Reset, "\n") - buffer.Append(t.CallSequence().Log().Elements()...) + + // Notify the user we failed to find anything + if t.status == TestCaseStatusFailed { + buffer.Append(fmt.Sprintf("Test for method \"%s.%s\" failed to identify a value greater than the minimum"+ + " value of an int256: ", t.targetContract.Name(), t.targetMethod.Sig)) + // We do not have a call sequence or execution trace for this test, so return early + return buffer } + + // We are guaranteed to now handle only successful test cases + buffer.Append(fmt.Sprintf("Test for method \"%s.%s\" resulted in the maximum value: ", t.targetContract.Name(), t.targetMethod.Sig)) + buffer.Append(colors.Bold, t.value, colors.Reset, "\n") + buffer.Append(colors.Bold, "[Call Sequence]", colors.Reset, "\n") + buffer.Append(t.CallSequence().Log().Elements()...) + // If an execution trace is attached then add it to the message if t.optimizationTestTrace != nil { buffer.Append(colors.Bold, "[Optimization Test Execution Trace]", colors.Reset, "\n") diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go index 38b8b772..0137c19a 100644 --- a/fuzzing/test_case_optimization_provider.go +++ b/fuzzing/test_case_optimization_provider.go @@ -12,6 +12,7 @@ import ( "golang.org/x/exp/slices" ) +// MIN_INT is the minimum value for an int256 in hexadecimal const MIN_INT = "-8000000000000000000000000000000000000000000000000000000000000000" // OptimizationTestCaseProvider is a provider for on-chain optimization tests. @@ -101,9 +102,9 @@ func (t *OptimizationTestCaseProvider) runOptimizationTest(worker *FuzzerWorker, return nil, nil, fmt.Errorf("failed to call optimization test method: %v", err) } - // If the execution reverted, then we know that we do not have any valuable return data, so we return the smallest - // integer value - if executionResult.Failed() { + // If the execution reverted or we have an empty return data, we know that we did not retrieve anything valuable + // so we maintain the minimum value + if executionResult.Failed() || len(executionResult.Return()) == 0 { minInt256, _ := new(big.Int).SetString(MIN_INT, 16) return minInt256, nil, nil } @@ -167,18 +168,10 @@ func (t *OptimizationTestCaseProvider) onFuzzerStarting(event FuzzerStartingEven } // onFuzzerStopping is the event handler triggered when the Fuzzer is stopping the fuzzing campaign and all workers -// have been destroyed. It clears state tracked for each FuzzerWorker and sets test cases in "running" states to -// "passed". +// have been destroyed. It clears state tracked for each FuzzerWorker. func (t *OptimizationTestCaseProvider) onFuzzerStopping(event FuzzerStoppingEvent) error { // Clear our optimization test methods t.workerStates = nil - - // Loop through each test case and set any tests with a running status to a passed status. - for _, testCase := range t.testCases { - if testCase.status == TestCaseStatusRunning { - testCase.status = TestCaseStatusPassed - } - } return nil } @@ -328,6 +321,7 @@ func (t *OptimizationTestCaseProvider) callSequencePostCallTest(worker *FuzzerWo // Create a request to shrink this call sequence. shrinkRequest := ShrinkCallSequenceRequest{ + TestName: testCase.Name(), CallSequenceToShrink: callSequence, VerifierFunction: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) (bool, error) { // First verify the contract to the optimization test is still deployed to call upon. diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index 9c265031..7f2bd91d 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -296,6 +296,7 @@ func (t *PropertyTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorker if failedPropertyTest { // Create a request to shrink this call sequence. shrinkRequest := ShrinkCallSequenceRequest{ + TestName: testCase.Name(), CallSequenceToShrink: callSequence, VerifierFunction: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) (bool, error) { // First verify the contract to property test is still deployed to call upon. diff --git a/fuzzing/testdata/contracts/cheat_codes/vm/difficulty.sol b/fuzzing/testdata/contracts/cheat_codes/vm/difficulty.sol index 097be7a2..22632415 100644 --- a/fuzzing/testdata/contracts/cheat_codes/vm/difficulty.sol +++ b/fuzzing/testdata/contracts/cheat_codes/vm/difficulty.sol @@ -1,4 +1,4 @@ -// This test ensures that the block difficulty can be set with cheat codes +// This test ensures that the difficulty cheatcode is a no-op interface CheatCodes { function difficulty(uint256) external; } @@ -8,10 +8,10 @@ contract TestContract { // Obtain our cheat code contract reference. CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - // Change value and verify. + uint256 originalDifficulty = block.difficulty; + // Update the difficulty cheats.difficulty(x); - assert(block.difficulty == x); - cheats.difficulty(7); - assert(block.difficulty == 7); + // Make sure that the new difficulty is the same as the original + assert(block.difficulty == originalDifficulty); } } diff --git a/fuzzing/testdata/contracts/cheat_codes/vm/fee_permanent.sol b/fuzzing/testdata/contracts/cheat_codes/vm/fee_permanent.sol index d9d6cf54..6293c155 100644 --- a/fuzzing/testdata/contracts/cheat_codes/vm/fee_permanent.sol +++ b/fuzzing/testdata/contracts/cheat_codes/vm/fee_permanent.sol @@ -8,15 +8,12 @@ contract TestContract { CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); uint256 newBaseFee = 42 gwei; - event TestFee(uint256 fee); - constructor() { // Set the new base fee cheats.fee(newBaseFee); } function test(uint256 x) public { - emit TestFee(block.basefee); // Assert that the change to fee is permanent assert(block.basefee == newBaseFee); } diff --git a/fuzzing/testdata/contracts/cheat_codes/vm/prevrandao.sol b/fuzzing/testdata/contracts/cheat_codes/vm/prevrandao.sol new file mode 100644 index 00000000..a434b167 --- /dev/null +++ b/fuzzing/testdata/contracts/cheat_codes/vm/prevrandao.sol @@ -0,0 +1,17 @@ +// This test ensures that the block random can be set with cheat codes +interface CheatCodes { + function prevrandao(bytes32) external; +} + +contract TestContract { + function test(uint256 x) public { + // Obtain our cheat code contract reference. + CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + // Change value and verify. + cheats.prevrandao(bytes32(uint256(42))); + assert(block.prevrandao == 42); + cheats.prevrandao(bytes32(uint256(420))); + assert(block.prevrandao == 420); + } +} diff --git a/fuzzing/testdata/contracts/cheat_codes/vm/roll_permanent.sol b/fuzzing/testdata/contracts/cheat_codes/vm/roll_permanent.sol index 8140c9a5..55cfe531 100644 --- a/fuzzing/testdata/contracts/cheat_codes/vm/roll_permanent.sol +++ b/fuzzing/testdata/contracts/cheat_codes/vm/roll_permanent.sol @@ -6,14 +6,16 @@ interface CheatCodes { contract TestContract { // Obtain our cheat code contract reference. CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - uint64 startingBlockNumber; + uint64 blockNumberOffset; constructor() { // Set the starting block number - startingBlockNumber = 12345; - cheats.roll(startingBlockNumber); + blockNumberOffset = 12345; + cheats.roll(block.number + blockNumberOffset); } function test(uint256 x) public { - assert(block.number > startingBlockNumber); + // We know that the block number originally will be 1 so we need + // to make sure that the new block number is 1 more than the offset + assert(block.number >= blockNumberOffset + 1); } } diff --git a/fuzzing/testdata/contracts/cheat_codes/vm/warp_permanent.sol b/fuzzing/testdata/contracts/cheat_codes/vm/warp_permanent.sol index 8e467569..677c5838 100644 --- a/fuzzing/testdata/contracts/cheat_codes/vm/warp_permanent.sol +++ b/fuzzing/testdata/contracts/cheat_codes/vm/warp_permanent.sol @@ -6,16 +6,18 @@ interface CheatCodes { contract TestContract { // Obtain our cheat code contract reference. CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - uint64 startingTimestamp; + uint64 timestampOffset; constructor() { // Set the starting timestamp - startingTimestamp = 12345; - cheats.warp(startingTimestamp); + timestampOffset = 12345; + cheats.warp(block.timestamp + 12345); } function test(uint64 x) public { - assert(block.timestamp > startingTimestamp); + // We know that the block timestamp originally will be 1 so we need + // to make sure that the new block timestamp is 1 more than the offset + assert(block.timestamp >= timestampOffset + 1); } } diff --git a/fuzzing/valuegeneration/generator_mutational.go b/fuzzing/valuegeneration/generator_mutational.go index 2933bfaf..3e648a88 100644 --- a/fuzzing/valuegeneration/generator_mutational.go +++ b/fuzzing/valuegeneration/generator_mutational.go @@ -275,11 +275,11 @@ func (g *MutationalValueGenerator) mutateBytesInternal(b []byte, length int) []b input = bytesMutationMethods[g.randomProvider.Intn(len(bytesMutationMethods))](g, input, inputs...) } - // If we want a fixed-byte array and the mutated input is smaller than the requested length, then generate a random - // byte array and append it to the existing input + // If we want a fixed-byte array and the mutated input is smaller than the requested length, pad the array + // with zeros if length > 0 && len(input) < length { - randomSlice := g.RandomValueGenerator.GenerateFixedBytes(length - len(input)) - input = append(input, randomSlice...) + paddedZeros := make([]byte, length-len(input)) + input = append(input, paddedZeros...) } // Similarly, if we want a fixed-byte array and the mutated input is larger than the requested length, then truncate