From 1e1bbe7b267a1950612466664546ba7346102300 Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 12 Feb 2022 15:03:29 -0500 Subject: [PATCH 01/14] fuzz dictionary --- evm-adapters/src/fuzz.rs | 247 ++++++++++++++---- evm-adapters/src/lib.rs | 7 + .../sputnik/cheatcodes/cheatcode_handler.rs | 20 +- .../cheatcodes/memory_stackstate_owned.rs | 52 +++- evm-adapters/src/sputnik/evm.rs | 13 +- evm-adapters/src/sputnik/mod.rs | 7 + 6 files changed, 276 insertions(+), 70 deletions(-) diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 0922468188aa..23ddd6b0e46c 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -6,7 +6,9 @@ use ethers::{ }; use std::{ cell::{RefCell, RefMut}, + collections::HashSet, marker::PhantomData, + rc::Rc, }; pub use proptest::test_runner::Config as FuzzConfig; @@ -62,14 +64,21 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { // fuzz test run. S: Clone, { - let strat = fuzz_calldata(func); + let strat = (60u32, fuzz_state_calldata(func, None)); // Snapshot the state before the test starts running let pre_test_state = self.evm.borrow().state().clone(); + let flattened_state = Rc::new(RefCell::new(self.evm.borrow().flatten_state())); + + // let select = proptest::sample::select(flattened_state.clone()); + let state_strat = (40u32, fuzz_state_calldata(func, Some(flattened_state.clone()))); + // stores the consumed gas and calldata of every successful fuzz call let fuzz_cases: RefCell> = RefCell::new(Default::default()); + let combined_strat = proptest::strategy::Union::new_weighted(vec![strat, state_strat]); + // stores the latest reason of a test call, this will hold the return reason of failed test // case if the runner failed let return_reason: RefCell> = RefCell::new(None); @@ -78,7 +87,7 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { let mut runner = self.runner.clone(); tracing::debug!(func = ?func.name, should_fail, "fuzzing"); let test_error = runner - .run(&strat, |calldata| { + .run(&combined_strat, |calldata| { let mut evm = self.evm.borrow_mut(); // Before each test, we must reset to the initial state evm.reset(pre_test_state.clone()); @@ -112,6 +121,12 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { } ); + { + let new_flattened = evm.flatten_state(); + let mut t = flattened_state.borrow_mut(); + (*t).extend(new_flattened); + } + // push test case to the case set fuzz_cases.borrow_mut().push(FuzzCase { calldata, gas }); @@ -222,10 +237,19 @@ pub struct FuzzCase { /// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata /// for that function's input types. -pub fn fuzz_calldata(func: &Function) -> impl Strategy + '_ { +pub fn fuzz_state_calldata<'a>( + func: &'a Function, + state: Option>>>, +) -> impl Strategy + '_ { // We need to compose all the strategies generated for each parameter in all // possible combinations - let strats = func.inputs.iter().map(|input| fuzz_param(&input.kind)).collect::>(); + // let strategy = proptest::sample::select(state.clone().into_iter().collect::>()); + let strats = func + .inputs + .iter() + .map(|input| fuzz_param_with_input(&input.kind, state.clone())) + .collect::>(); strats.prop_map(move |tokens| { tracing::trace!(input = ?tokens); @@ -236,65 +260,176 @@ pub fn fuzz_calldata(func: &Function) -> impl Strategy + '_ { /// The max length of arrays we fuzz for is 256. const MAX_ARRAY_LEN: usize = 256; -/// Given an ethabi parameter type, returns a proptest strategy for generating values for that -/// datatype. Works with ABI Encoder v2 tuples. -fn fuzz_param(param: &ParamType) -> impl Strategy { - match param { - ParamType::Address => { - // The key to making this work is the `boxed()` call which type erases everything - // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html - any::<[u8; 20]>().prop_map(|x| Address::from_slice(&x).into_token()).boxed() - } - ParamType::Bytes => any::>().prop_map(|x| Bytes::from(x).into_token()).boxed(), - // For ints and uints we sample from a U256, then wrap it to the correct size with a - // modulo operation. Note that this introduces modulo bias, but it can be removed with - // rejection sampling if it's determined the bias is too severe. Rejection sampling may - // slow down tests as it resamples bad values, so may want to benchmark the performance - // hit and weigh that against the current bias before implementing - ParamType::Int(n) => match n / 8 { - 32 => any::<[u8; 32]>() - .prop_map(move |x| I256::from_raw(U256::from(&x)).into_token()) +fn fuzz_param_with_input( + param: &ParamType, + state: Option>>>, +) -> impl Strategy { + use proptest::prelude::*; + if let Some(state) = state { + let selectors = any::(); + match param { + ParamType::Address => { + selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Address::from_slice(&x[..]).into_token() + }) + .boxed() + // The key to making this work is the `boxed()` call which type erases everything + // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html + // state.prop_map(|x| ).boxed() + } + ParamType::Bytes => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Bytes::from(x).into_token() + }) .boxed(), - y @ 1..=31 => any::<[u8; 32]>() - .prop_map(move |x| { - // Generate a uintN in the correct range, then shift it to the range of intN - // by subtracting 2^(N-1) - let uint = U256::from(&x) % U256::from(2).pow(U256::from(y * 8)); - let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); - let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); - num.into_token() + // For ints and uints we sample from a U256, then wrap it to the correct size with a + // modulo operation. Note that this introduces modulo bias, but it can be removed with + // rejection sampling if it's determined the bias is too severe. Rejection sampling may + // slow down tests as it resamples bad values, so may want to benchmark the performance + // hit and weigh that against the current bias before implementing + ParamType::Int(n) => match n / 8 { + 32 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + I256::from_raw(U256::from(x)).into_token() + }) + .boxed(), + y @ 1..=31 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + // Generate a uintN in the correct range, then shift it to the range of intN + // by subtracting 2^(N-1) + let uint = U256::from(x) % U256::from(2).pow(U256::from(y * 8)); + let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); + let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); + num.into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type int{}", n), + }, + ParamType::Uint(n) => match n / 8 { + 32 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + U256::from(x).into_token() + }) + .boxed(), + y @ 1..=31 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + (U256::from(x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type uint{}", n), + }, + ParamType::Bool => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Token::Bool(x[31] == 1) }) .boxed(), - _ => panic!("unsupported solidity type int{}", n), - }, - ParamType::Uint(n) => match n / 8 { - 32 => any::<[u8; 32]>().prop_map(move |x| U256::from(&x).into_token()).boxed(), - y @ 1..=31 => any::<[u8; 32]>() - .prop_map(move |x| { - (U256::from(&x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() + ParamType::String => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) }) .boxed(), - _ => panic!("unsupported solidity type uint{}", n), - }, - ParamType::Bool => any::().prop_map(|x| x.into_token()).boxed(), - ParamType::String => any::>() - .prop_map(|x| Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() })) - .boxed(), - ParamType::Array(param) => proptest::collection::vec(fuzz_param(param), 0..MAX_ARRAY_LEN) + ParamType::Array(param) => proptest::collection::vec( + fuzz_param_with_input(param, Some(state)), + 0..MAX_ARRAY_LEN, + ) .prop_map(Token::Array) .boxed(), - ParamType::FixedBytes(size) => (0..*size as u64) - .map(|_| any::()) - .collect::>() - .prop_map(Token::FixedBytes) - .boxed(), - ParamType::FixedArray(param, size) => (0..*size as u64) - .map(|_| fuzz_param(param).prop_map(|param| param.into_token())) - .collect::>() - .prop_map(Token::FixedArray) - .boxed(), - ParamType::Tuple(params) => { - params.iter().map(fuzz_param).collect::>().prop_map(Token::Tuple).boxed() + ParamType::FixedBytes(ref _size) => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + // TODO: figure out if size is actually needed here? + Token::FixedBytes(x.to_vec()) + }) + .boxed(), + ParamType::FixedArray(param, size) => (0..*size as u64) + .map(|_| { + fuzz_param_with_input(param, Some(state.clone())) + .prop_map(|param| param.into_token()) + }) + .collect::>() + .prop_map(Token::FixedArray) + .boxed(), + ParamType::Tuple(params) => params + .iter() + .map(|p| fuzz_param_with_input(p, Some(state.clone()))) + .collect::>() + .prop_map(Token::Tuple) + .boxed(), + } + } else { + match param { + ParamType::Address => { + // The key to making this work is the `boxed()` call which type erases everything + // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html + any::<[u8; 20]>().prop_map(|x| Address::from_slice(&x).into_token()).boxed() + } + ParamType::Bytes => any::>().prop_map(|x| Bytes::from(x).into_token()).boxed(), + // For ints and uints we sample from a U256, then wrap it to the correct size with a + // modulo operation. Note that this introduces modulo bias, but it can be removed with + // rejection sampling if it's determined the bias is too severe. Rejection sampling may + // slow down tests as it resamples bad values, so may want to benchmark the performance + // hit and weigh that against the current bias before implementing + ParamType::Int(n) => match n / 8 { + 32 => any::<[u8; 32]>() + .prop_map(move |x| I256::from_raw(U256::from(&x)).into_token()) + .boxed(), + y @ 1..=31 => any::<[u8; 32]>() + .prop_map(move |x| { + // Generate a uintN in the correct range, then shift it to the range of intN + // by subtracting 2^(N-1) + let uint = U256::from(&x) % U256::from(2).pow(U256::from(y * 8)); + let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); + let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); + num.into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type int{}", n), + }, + ParamType::Uint(n) => match n / 8 { + 32 => any::<[u8; 32]>().prop_map(move |x| U256::from(&x).into_token()).boxed(), + y @ 1..=31 => any::<[u8; 32]>() + .prop_map(move |x| { + (U256::from(&x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type uint{}", n), + }, + ParamType::Bool => any::().prop_map(|x| x.into_token()).boxed(), + ParamType::String => any::>() + .prop_map(|x| { + Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) + }) + .boxed(), + ParamType::Array(param) => { + proptest::collection::vec(fuzz_param_with_input(param, None), 0..MAX_ARRAY_LEN) + .prop_map(Token::Array) + .boxed() + } + ParamType::FixedBytes(size) => (0..*size as u64) + .map(|_| any::()) + .collect::>() + .prop_map(Token::FixedBytes) + .boxed(), + ParamType::FixedArray(param, size) => (0..*size as u64) + .map(|_| fuzz_param_with_input(param, None).prop_map(|param| param.into_token())) + .collect::>() + .prop_map(Token::FixedArray) + .boxed(), + ParamType::Tuple(params) => params + .iter() + .map(|p| fuzz_param_with_input(p, None)) + .collect::>() + .prop_map(Token::Tuple) + .boxed(), } } } diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index 19510226621a..5fb35ba367a1 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -7,6 +7,7 @@ use crate::sputnik::cheatcodes::debugger::DebugArena; mod blocking_provider; use crate::call_tracing::CallTraceArena; +use std::collections::HashSet; pub use blocking_provider::BlockingProvider; @@ -222,6 +223,12 @@ pub trait Evm { // TODO: Should we add a "deploy contract" function as well, or should we assume that // the EVM is instantiated with a DB that includes any needed contracts? + + fn flatten_state(&self) -> HashSet<[u8; 32]>; +} + +pub trait FuzzState { + fn flatten_state(&self) -> HashSet<[u8; 32]>; } // Test helpers which are generic over EVM implementation diff --git a/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs b/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs index 6b4eafc8e547..0ccd48ebb03c 100644 --- a/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs +++ b/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs @@ -8,7 +8,7 @@ use crate::{ sputnik::{cheatcodes::memory_stackstate_owned::ExpectedEmit, Executor, SputnikExecutor}, Evm, }; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use std::{fs::File, io::Read, path::Path}; @@ -34,14 +34,16 @@ use ethers::{ use std::{convert::Infallible, str::FromStr}; -use crate::sputnik::cheatcodes::{ - debugger::{CheatOp, DebugArena, DebugNode, DebugStep, OpCode}, - memory_stackstate_owned::Prank, - patch_hardhat_console_log_selector, +use crate::{ + sputnik::cheatcodes::{ + debugger::{CheatOp, DebugArena, DebugNode, DebugStep, OpCode}, + memory_stackstate_owned::Prank, + patch_hardhat_console_log_selector, + }, + FuzzState, }; -use once_cell::sync::Lazy; - use ethers::abi::Tokenize; +use once_cell::sync::Lazy; // This is now getting us the right hash? Also tried [..20] // Lazy::new(|| Address::from_slice(&keccak256("hevm cheat code")[12..])); @@ -339,6 +341,10 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> SputnikExecutor HashSet<[u8; 32]> { + self.state().flatten_state() + } } /// A [`MemoryStackStateOwned`] state instantiated over a [`CheatcodeBackend`] diff --git a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs index 31db29635900..db084bcea544 100644 --- a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs +++ b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs @@ -1,17 +1,20 @@ use sputnik::{ - backend::{Backend, Basic}, + backend::{Apply, Backend, Basic}, executor::stack::{MemoryStackSubstate, StackState, StackSubstateMetadata}, ExitError, Transfer, }; -use crate::{call_tracing::CallTraceArena, sputnik::cheatcodes::debugger::DebugArena}; - +use crate::{call_tracing::CallTraceArena, sputnik::cheatcodes::debugger::DebugArena, FuzzState}; use ethers::{ abi::RawLog, types::{H160, H256, U256}, }; -use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; +use std::{ + cell::RefCell, + collections::{BTreeMap, HashSet}, + rc::Rc, +}; #[derive(Clone, Default)] pub struct RecordAccess { @@ -150,6 +153,47 @@ impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { } } +impl<'config, B: Backend> FuzzState for MemoryStackStateOwned<'config, B> { + fn flatten_state(&self) -> HashSet<[u8; 32]> { + let mut flattened: HashSet<[u8; 32]> = HashSet::new(); + let (applies, logs) = self.substate.clone().deconstruct(&self.backend); + for apply in applies { + if let Apply::Modify { address, basic, storage, ..} = apply { + let old_basic = self.basic(address); + flattened.insert(H256::from(address).into()); + // we keep bytes as little endian + let mut h = H256::default(); + old_basic.balance.to_little_endian(h.as_mut()); + flattened.insert(h.0); + let mut h = H256::default(); + old_basic.nonce.to_little_endian(h.as_mut()); + flattened.insert(h.0); + let mut h = H256::default(); + basic.balance.to_little_endian(h.as_mut()); + flattened.insert(h.0); + let mut h = H256::default(); + basic.nonce.to_little_endian(h.as_mut()); + flattened.insert(h.0); + storage.into_iter().for_each(|(slot, store)| { + flattened.insert(self.storage(address, slot).0); + flattened.insert(slot.0); + flattened.insert(store.0); + }); + } + } + for log in logs { + flattened.insert(H256::from(log.address).into()); + log.topics.iter().for_each(|topic| { + flattened.insert(topic.0); + }); + log.data.chunks(32).for_each(|chunk| { + flattened.insert(H256::from_slice(chunk).into()); + }); + } + flattened + } +} + impl<'config, B: Backend> Backend for MemoryStackStateOwned<'config, B> { fn gas_price(&self) -> U256 { self.backend.gas_price() diff --git a/evm-adapters/src/sputnik/evm.rs b/evm-adapters/src/sputnik/evm.rs index 36e37871d8fd..83dd66e2224a 100644 --- a/evm-adapters/src/sputnik/evm.rs +++ b/evm-adapters/src/sputnik/evm.rs @@ -1,4 +1,4 @@ -use crate::{call_tracing::CallTraceArena, Evm, FAUCET_ACCOUNT}; +use crate::{call_tracing::CallTraceArena, Evm, FuzzState, FAUCET_ACCOUNT}; use ethers::types::{Address, Bytes, U256}; use crate::sputnik::cheatcodes::debugger::DebugArena; @@ -10,7 +10,10 @@ use sputnik::{ }, Config, CreateScheme, ExitReason, ExitRevert, Transfer, }; -use std::{collections::BTreeMap, marker::PhantomData}; +use std::{ + collections::{BTreeMap, HashSet}, + marker::PhantomData, +}; use eyre::Result; @@ -60,7 +63,7 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> impl<'a, S, E> Evm for Executor where E: SputnikExecutor, - S: StackState<'a>, + S: StackState<'a> + FuzzState, { type ReturnReason = ExitReason; @@ -206,6 +209,10 @@ where Ok((retdata.into(), status, gas.as_u64(), logs)) } + + fn flatten_state(&self) -> HashSet<[u8; 32]> { + self.executor.flatten_state() + } } #[cfg(any(test, feature = "sputnik-helpers"))] diff --git a/evm-adapters/src/sputnik/mod.rs b/evm-adapters/src/sputnik/mod.rs index 69da44c674f7..106c572413d1 100644 --- a/evm-adapters/src/sputnik/mod.rs +++ b/evm-adapters/src/sputnik/mod.rs @@ -23,6 +23,7 @@ use crate::{call_tracing::CallTraceArena, sputnik::cheatcodes::debugger::DebugAr pub use sputnik as sputnik_evm; use sputnik_evm::executor::stack::PrecompileSet; +use std::collections::HashSet; /// Given an ethers provider and a block, it proceeds to construct a [`MemoryVicinity`] from /// the live chain data returned by the provider. @@ -111,6 +112,8 @@ pub trait SputnikExecutor { /// Clears all logs in the current EVM instance, so that subsequent calls to /// `logs` do not print duplicate logs on shared EVM instances. fn clear_logs(&mut self); + + fn flatten_state(&self) -> HashSet<[u8; 32]>; } // The implementation for the base Stack Executor just forwards to the internal methods. @@ -199,6 +202,10 @@ impl<'a, 'b, S: StackState<'a>, P: PrecompileSet> SputnikExecutor } fn clear_logs(&mut self) {} + + fn flatten_state(&self) -> HashSet<[u8; 32]> { + HashSet::new() + } } use std::borrow::Cow; From bf9983809dbda6101b7fbf79d1ceefd67e7774db Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 5 Mar 2022 08:52:17 -0700 Subject: [PATCH 02/14] updates --- evm-adapters/src/fuzz.rs | 19 ++++++++++++++++++- evm-adapters/testdata/Fuzz.sol | 13 +++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 23ddd6b0e46c..861f07ac533f 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -272,7 +272,7 @@ fn fuzz_param_with_input( selectors .prop_map(move |selector| { let x = *selector.select(&*state.borrow()); - Address::from_slice(&x[..]).into_token() + Address::from_slice(&x[12..]).into_token() }) .boxed() // The key to making this work is the `boxed()` call which type erases everything @@ -461,4 +461,21 @@ mod tests { let revert_reason = error.revert_reason; assert_eq!(revert_reason, "fuzztest-revert"); } + + #[test] + fn finds_fuzzed_state_revert() { + let mut evm = vm(); + + let compiled = COMPILED.find("FuzzTests").expect("could not find contract"); + let (addr, _, _, _) = + evm.deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()).unwrap(); + + let evm = fuzzvm(&mut evm); + + let func = compiled.abi.unwrap().function("testFuzzedStateRevert").unwrap(); + let res = evm.fuzz(func, addr, false, compiled.abi); + let error = res.test_error.unwrap(); + let revert_reason = error.revert_reason; + assert_eq!(revert_reason, "fuzzstate-revert"); + } } diff --git a/evm-adapters/testdata/Fuzz.sol b/evm-adapters/testdata/Fuzz.sol index e5552b4a4159..e7513d12a361 100644 --- a/evm-adapters/testdata/Fuzz.sol +++ b/evm-adapters/testdata/Fuzz.sol @@ -1,7 +1,20 @@ pragma solidity ^0.8.10; +contract C { + address stateAddr = address(1337); + + function t(address _t) public { + require(_t != stateAddr, "fuzzstate-revert"); + } +} + contract FuzzTests { function testFuzzedRevert(uint256 x) public { require(x == 5, "fuzztest-revert"); } + + function testFuzzedStateRevert(address x) public { + C c = new C(); + c.t(x); + } } From bdd84eb997e39016a1295af4879edee5d5bbd449 Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 5 Mar 2022 08:57:51 -0700 Subject: [PATCH 03/14] readd uint strat --- evm-adapters/src/fuzz.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 652b5c7fef31..6dd4c2e94109 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -405,14 +405,8 @@ fn fuzz_param_with_input( .boxed(), _ => panic!("unsupported solidity type int{}", n), }, - ParamType::Uint(n) => match n / 8 { - 32 => any::<[u8; 32]>().prop_map(move |x| U256::from(&x).into_token()).boxed(), - y @ 1..=31 => any::<[u8; 32]>() - .prop_map(move |x| { - (U256::from(&x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() - }) - .boxed(), - _ => panic!("unsupported solidity type uint{}", n), + ParamType::Uint(n) => { + strategies::UintStrategy::new(*n, vec![]).prop_map(|x| x.into_token()).boxed() }, ParamType::Bool => any::().prop_map(|x| x.into_token()).boxed(), ParamType::String => any::>() From c725eed5ab577e8202d3f8eaa0b1b8f176291e8f Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 5 Mar 2022 09:18:31 -0700 Subject: [PATCH 04/14] fmt + remove dead rpc backend --- evm-adapters/src/fuzz.rs | 2 +- .../cheatcodes/memory_stackstate_owned.rs | 2 +- .../src/sputnik/forked_backend/mod.rs | 2 - .../src/sputnik/forked_backend/rpc.rs | 251 ------------------ 4 files changed, 2 insertions(+), 255 deletions(-) delete mode 100644 evm-adapters/src/sputnik/forked_backend/rpc.rs diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 6dd4c2e94109..07c74f8f63bb 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -407,7 +407,7 @@ fn fuzz_param_with_input( }, ParamType::Uint(n) => { strategies::UintStrategy::new(*n, vec![]).prop_map(|x| x.into_token()).boxed() - }, + } ParamType::Bool => any::().prop_map(|x| x.into_token()).boxed(), ParamType::String => any::>() .prop_map(|x| { diff --git a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs index db084bcea544..eeed2f8d5776 100644 --- a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs +++ b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs @@ -158,7 +158,7 @@ impl<'config, B: Backend> FuzzState for MemoryStackStateOwned<'config, B> { let mut flattened: HashSet<[u8; 32]> = HashSet::new(); let (applies, logs) = self.substate.clone().deconstruct(&self.backend); for apply in applies { - if let Apply::Modify { address, basic, storage, ..} = apply { + if let Apply::Modify { address, basic, storage, .. } = apply { let old_basic = self.basic(address); flattened.insert(H256::from(address).into()); // we keep bytes as little endian diff --git a/evm-adapters/src/sputnik/forked_backend/mod.rs b/evm-adapters/src/sputnik/forked_backend/mod.rs index e66cd2b3629b..7a238bf47de3 100644 --- a/evm-adapters/src/sputnik/forked_backend/mod.rs +++ b/evm-adapters/src/sputnik/forked_backend/mod.rs @@ -1,4 +1,2 @@ pub mod cache; pub use cache::{new_shared_cache, MemCache, SharedBackend, SharedCache}; -pub mod rpc; -pub use rpc::ForkMemoryBackend; diff --git a/evm-adapters/src/sputnik/forked_backend/rpc.rs b/evm-adapters/src/sputnik/forked_backend/rpc.rs deleted file mode 100644 index bda797ec1dcd..000000000000 --- a/evm-adapters/src/sputnik/forked_backend/rpc.rs +++ /dev/null @@ -1,251 +0,0 @@ -//! Simple in-memory cache backend for use with forking providers -use std::{cell::RefCell, collections::BTreeMap}; - -use ethers::{ - providers::Middleware, - types::{Block, BlockId, BlockNumber, TxHash, H160, H256, U256}, -}; -use sputnik::backend::{Backend, Basic, MemoryAccount}; - -use crate::BlockingProvider; - -/// Memory backend with ability to fork another chain from an HTTP provider, storing all cache -/// values in a `BTreeMap` in memory. -// TODO: Add option to easily 1. impersonate accounts, 2. roll back to pinned block -// TODO: In order to improve speed, does it make sense to add a job which pre-fetches -// accounts speculatively? Or maybe do it for smart contract code which is typically the -// biggest issue? -// TODO: In order to improve speed, can we instead write a custom blocking provider which -// does not block_on in-line, but has a background thread that polls everything in parallel -// and just returns the results synchronously via some channel? -pub struct ForkMemoryBackend { - /// ethers middleware for querying on-chain data - pub provider: BlockingProvider, - /// The internal backend - pub backend: B, - /// cache state - // TODO: Actually cache values in memory. - // TODO: This should probably be abstracted away into something that efficiently - // also caches at disk etc. - pub cache: RefCell>, - /// The block to fetch data from. - // This is an `Option` so that we can have less code churn in the functions below - pin_block: Option, - /// The block at which we forked off - pin_block_meta: Block, - /// The chain id of the forked chain - chain_id: U256, -} - -impl ForkMemoryBackend -where - M::Error: 'static, -{ - pub fn new( - provider: M, - backend: B, - pin_block: Option, - init_cache: BTreeMap, - ) -> Self { - let provider = BlockingProvider::new(provider); - - // get the remaining block metadata - let (block, chain_id) = - provider.block_and_chainid(pin_block).expect("could not get block meta and chain id"); - - Self { - provider, - backend, - cache: RefCell::new(init_cache), - pin_block: pin_block.map(Into::into), - pin_block_meta: block, - chain_id, - } - } -} - -impl Backend for ForkMemoryBackend -where - M::Error: 'static, -{ - fn gas_price(&self) -> U256 { - self.backend.gas_price() - } - - fn origin(&self) -> H160 { - self.backend.origin() - } - - fn block_hash(&self, number: U256) -> H256 { - self.backend.block_hash(number) - } - - fn block_number(&self) -> U256 { - self.pin_block - .and_then(|block| match block { - BlockId::Number(num) => match num { - BlockNumber::Number(num) => Some(num.as_u64().into()), - _ => None, - }, - BlockId::Hash(_) => None, - }) - .unwrap_or_else(|| self.backend.block_number()) - } - - fn block_coinbase(&self) -> H160 { - self.pin_block_meta.author - } - - fn block_timestamp(&self) -> U256 { - self.pin_block_meta.timestamp - } - - fn block_difficulty(&self) -> U256 { - self.pin_block_meta.difficulty - } - - fn block_gas_limit(&self) -> U256 { - self.pin_block_meta.gas_limit - } - - fn block_base_fee_per_gas(&self) -> U256 { - self.pin_block_meta.base_fee_per_gas.unwrap_or_default() - } - - fn chain_id(&self) -> U256 { - self.chain_id - } - - fn exists(&self, address: H160) -> bool { - let mut exists = self.cache.borrow().contains_key(&address); - - // check non-zero balance - if !exists { - let mut cache = self.cache.borrow_mut(); - let account = cache.entry(address).or_insert_with(|| { - let res = self.provider.get_account(address, self.pin_block).unwrap_or_default(); - MemoryAccount { - nonce: res.0, - balance: res.1, - code: res.2.to_vec(), - storage: Default::default(), - } - }); - exists = account.balance != U256::zero() || - account.nonce != U256::zero() || - !account.code.is_empty(); - } - - exists - } - - fn basic(&self, address: H160) -> Basic { - let mut cache = self.cache.borrow_mut(); - let account = cache.entry(address).or_insert_with(|| { - let res = self.provider.get_account(address, self.pin_block).unwrap_or_default(); - MemoryAccount { - nonce: res.0, - balance: res.1, - code: res.2.to_vec(), - storage: Default::default(), - } - }); - Basic { balance: account.balance, nonce: account.nonce } - } - - fn code(&self, address: H160) -> Vec { - let mut cache = self.cache.borrow_mut(); - let account = cache.entry(address).or_insert_with(|| { - // println!("didnt have account code {:?}", address); - let res = self.provider.get_account(address, self.pin_block).unwrap_or_default(); - MemoryAccount { - nonce: res.0, - balance: res.1, - code: res.2.to_vec(), - storage: Default::default(), - } - }); - account.code.clone() - } - - fn storage(&self, address: H160, index: H256) -> H256 { - let mut cache = self.cache.borrow_mut(); - let account = cache.entry(address).or_insert_with(|| { - let res = self.provider.get_account(address, self.pin_block).unwrap_or_default(); - MemoryAccount { - nonce: res.0, - balance: res.1, - code: res.2.to_vec(), - storage: Default::default(), - } - }); - if let Some(val) = account.storage.get(&index) { - *val - } else { - let ret = - self.provider.get_storage_at(address, index, self.pin_block).unwrap_or_default(); - account.storage.insert(index, ret); - ret - } - } - - fn original_storage(&self, address: H160, index: H256) -> Option { - Some(self.storage(address, index)) - } -} - -#[cfg(test)] -mod tests { - use std::convert::TryFrom; - - use ethers::{ - providers::{Http, Provider}, - types::Address, - }; - use sputnik::Config; - use tokio::runtime::Runtime; - - use crate::{ - sputnik::{helpers::new_backend, vicinity, Executor, PRECOMPILES_MAP}, - test_helpers::COMPILED, - Evm, - }; - - use super::*; - - #[test] - fn forked_backend() { - let cfg = Config::london(); - let compiled = COMPILED.find("Greeter").expect("could not find contract"); - - let provider = Provider::::try_from( - "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", - ) - .unwrap(); - let rt = Runtime::new().unwrap(); - let blk = Some(13292465); - let vicinity = rt.block_on(vicinity(&provider, None, blk, None)).unwrap(); - let backend = new_backend(&vicinity, Default::default()); - let backend = ForkMemoryBackend::new(provider, backend, blk, Default::default()); - - let precompiles = PRECOMPILES_MAP.clone(); - let mut evm = Executor::new(12_000_000, &cfg, &backend, &precompiles); - - let (addr, _, _, _) = - evm.deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()).unwrap(); - - let (res, _, _, _) = evm - .call::( - Address::zero(), - addr, - "time()(uint256)", - (), - 0.into(), - compiled.abi, - ) - .unwrap(); - - // https://etherscan.io/block/13292465 - assert_eq!(res.as_u64(), 1632539668); - } -} From 880b5b8c9717cb74245f83c0035463e84c43ad1d Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 5 Mar 2022 09:36:08 -0700 Subject: [PATCH 05/14] fixed bytes fix, comment cleanup --- evm-adapters/src/fuzz.rs | 44 +++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 07c74f8f63bb..bea214a2b1dd 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -276,31 +276,23 @@ fn fuzz_param_with_input( state: Option>>>, ) -> impl Strategy { use proptest::prelude::*; + // The key to making this work is the `boxed()` call which type erases everything + // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html if let Some(state) = state { let selectors = any::(); match param { - ParamType::Address => { - selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Address::from_slice(&x[12..]).into_token() - }) - .boxed() - // The key to making this work is the `boxed()` call which type erases everything - // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html - // state.prop_map(|x| ).boxed() - } + ParamType::Address => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Address::from_slice(&x[12..]).into_token() + }) + .boxed(), ParamType::Bytes => selectors .prop_map(move |selector| { let x = *selector.select(&*state.borrow()); Bytes::from(x).into_token() }) .boxed(), - // For ints and uints we sample from a U256, then wrap it to the correct size with a - // modulo operation. Note that this introduces modulo bias, but it can be removed with - // rejection sampling if it's determined the bias is too severe. Rejection sampling may - // slow down tests as it resamples bad values, so may want to benchmark the performance - // hit and weigh that against the current bias before implementing ParamType::Int(n) => match n / 8 { 32 => selectors .prop_map(move |selector| { @@ -354,13 +346,17 @@ fn fuzz_param_with_input( ) .prop_map(Token::Array) .boxed(), - ParamType::FixedBytes(ref _size) => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - // TODO: figure out if size is actually needed here? - Token::FixedBytes(x.to_vec()) - }) - .boxed(), + ParamType::FixedBytes(size) => { + // we have to clone outside the prop_map to satisfy lifetime constraints + let v = size.clone(); + selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + let val = x[32 - v..].to_vec(); + Token::FixedBytes(val) + }) + .boxed() + } ParamType::FixedArray(param, size) => (0..*size as u64) .map(|_| { fuzz_param_with_input(param, Some(state.clone())) @@ -379,8 +375,6 @@ fn fuzz_param_with_input( } else { match param { ParamType::Address => { - // The key to making this work is the `boxed()` call which type erases everything - // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html any::<[u8; 20]>().prop_map(|x| Address::from_slice(&x).into_token()).boxed() } ParamType::Bytes => any::>().prop_map(|x| Bytes::from(x).into_token()).boxed(), From 73949ca802637c930c6d9c2e8b6f330b939f552a Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 5 Mar 2022 09:39:06 -0700 Subject: [PATCH 06/14] clippy --- evm-adapters/src/fuzz.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index bea214a2b1dd..c9513d08cbe7 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -348,7 +348,7 @@ fn fuzz_param_with_input( .boxed(), ParamType::FixedBytes(size) => { // we have to clone outside the prop_map to satisfy lifetime constraints - let v = size.clone(); + let v = *size; selectors .prop_map(move |selector| { let x = *selector.select(&*state.borrow()); From c585a720d9b3cda5d6f7cc0af09ad1d6d9313d06 Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 5 Mar 2022 11:04:35 -0700 Subject: [PATCH 07/14] weighting parameterization + noshrink for state --- cli/src/cmd/run.rs | 4 ++ evm-adapters/src/fuzz.rs | 47 +++++++++++++++---- .../sputnik/cheatcodes/cheatcode_handler.rs | 2 +- evm-adapters/src/sputnik/evm.rs | 2 +- forge/src/multi_runner.rs | 2 + forge/src/runner.rs | 41 +++++++++++++--- 6 files changed, 80 insertions(+), 18 deletions(-) diff --git a/cli/src/cmd/run.rs b/cli/src/cmd/run.rs index b4f0603ee13d..72a64f3ff997 100644 --- a/cli/src/cmd/run.rs +++ b/cli/src/cmd/run.rs @@ -115,6 +115,8 @@ impl Cmd for RunArgs { Some(evm_opts.sender), None, &predeploy_libraries, + 0, + 0, ); runner.run_test(&func, needs_setup, Some(&known_contracts))? } @@ -128,6 +130,8 @@ impl Cmd for RunArgs { Some(evm_opts.sender), None, &predeploy_libraries, + 0, + 0, ); runner.run_test(&func, needs_setup, Some(&known_contracts))? } diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index c9513d08cbe7..3796634b9e63 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -31,6 +31,8 @@ pub struct FuzzedExecutor<'a, E, S> { runner: TestRunner, state: PhantomData, sender: Address, + pub state_weight: u32, + pub random_weight: u32, } impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { @@ -44,8 +46,21 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { } /// Instantiates a fuzzed executor EVM given a testrunner - pub fn new(evm: &'a mut E, runner: TestRunner, sender: Address) -> Self { - Self { evm: RefCell::new(evm), runner, state: PhantomData, sender } + pub fn new( + evm: &'a mut E, + runner: TestRunner, + sender: Address, + state_weight: u32, + random_weight: u32, + ) -> Self { + Self { + evm: RefCell::new(evm), + runner, + state: PhantomData, + sender, + state_weight, + random_weight, + } } /// Fuzzes the provided function, assuming it is available at the contract at `address` @@ -66,20 +81,34 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { // fuzz test run. S: Clone, { - let strat = (60u32, fuzz_state_calldata(func, None)); + let mut strats = Vec::new(); + if self.random_weight > 0 { + strats.push((self.random_weight, fuzz_state_calldata(func.clone(), None).boxed())) + } // Snapshot the state before the test starts running let pre_test_state = self.evm.borrow().state().clone(); let flattened_state = Rc::new(RefCell::new(self.evm.borrow().flatten_state())); - // let select = proptest::sample::select(flattened_state.clone()); - let state_strat = (40u32, fuzz_state_calldata(func, Some(flattened_state.clone()))); + // we dont shrink for state strategy + if self.state_weight > 0 { + strats.push(( + self.state_weight, + fuzz_state_calldata(func.clone(), Some(flattened_state.clone())) + .no_shrink() + .boxed(), + )) + } + + if strats.is_empty() { + panic!("Fuzz strategy weights were all 0. Please set at least one strategy weight to be above 0"); + } // stores the consumed gas and calldata of every successful fuzz call let fuzz_cases: RefCell> = RefCell::new(Default::default()); - let combined_strat = proptest::strategy::Union::new_weighted(vec![strat, state_strat]); + let combined_strat = proptest::strategy::Union::new_weighted(strats); // stores the latest reason of a test call, this will hold the return reason of failed test // case if the runner failed @@ -248,10 +277,10 @@ pub struct FuzzCase { /// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata /// for that function's input types. -pub fn fuzz_state_calldata<'a>( - func: &'a Function, +pub fn fuzz_state_calldata( + func: Function, state: Option>>>, -) -> impl Strategy + '_ { +) -> impl Strategy { // We need to compose all the strategies generated for each parameter in all // possible combinations // let strategy = proptest::sample::select(state.clone().into_iter().collect:: MemoryBackend<'_> { diff --git a/forge/src/multi_runner.rs b/forge/src/multi_runner.rs index 596a3088a0df..d77c2ef34199 100644 --- a/forge/src/multi_runner.rs +++ b/forge/src/multi_runner.rs @@ -258,6 +258,8 @@ impl MultiContractRunner { self.sender, Some((&self.execution_info.0, &self.execution_info.1, &self.execution_info.2)), libs, + 40, + 60, ); runner.run_tests(filter, self.fuzzer.clone(), Some(&self.known_contracts)) } diff --git a/forge/src/runner.rs b/forge/src/runner.rs index 80d508b701f1..9896403d1b7b 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -169,6 +169,10 @@ pub struct ContractRunner<'a, B> { pub execution_info: MaybeExecutionInfo<'a>, /// library contracts to be deployed before this contract pub predeploy_libs: &'a [ethers::prelude::Bytes], + /// Fuzzing weighting for using state lifted values + pub state_weight: u32, + /// Fuzzing weighting for using random values + pub random_weight: u32, } impl<'a, B: Backend> ContractRunner<'a, B> { @@ -182,6 +186,8 @@ impl<'a, B: Backend> ContractRunner<'a, B> { sender: Option
, execution_info: MaybeExecutionInfo<'a>, predeploy_libs: &'a [ethers::prelude::Bytes], + state_weight: u32, + random_weight: u32, ) -> Self { Self { evm_opts, @@ -192,6 +198,8 @@ impl<'a, B: Backend> ContractRunner<'a, B> { sender: sender.unwrap_or_default(), execution_info, predeploy_libs, + state_weight, + random_weight, } } } @@ -462,7 +470,13 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { let prev = evm.set_tracing_enabled(false); // instantiate the fuzzed evm in line - let evm = FuzzedExecutor::new(&mut evm, runner, self.sender); + let evm = FuzzedExecutor::new( + &mut evm, + runner, + self.sender, + self.state_weight, + self.random_weight, + ); let FuzzTestResult { cases, test_error } = evm.fuzz(func, address, should_fail, Some(self.contract)); @@ -607,8 +621,21 @@ mod tests { abi: &'a Abi, code: ethers::prelude::Bytes, libs: &'a mut Vec, + state_weight: u32, + random_weight: u32, ) -> ContractRunner<'a, MemoryBackend<'a>> { - ContractRunner::new(&*EVM_OPTS, &*CFG_NO_LMT, &*BACKEND, abi, code, None, None, libs) + ContractRunner::new( + &*EVM_OPTS, + &*CFG_NO_LMT, + &*BACKEND, + abi, + code, + None, + None, + libs, + state_weight, + random_weight, + ) } #[test] @@ -623,7 +650,7 @@ mod tests { let (_, code, _) = compiled.into_parts_or_default(); let mut libs = vec![]; - let runner = runner(compiled.abi.as_ref().unwrap(), code, &mut libs); + let runner = runner(compiled.abi.as_ref().unwrap(), code, &mut libs, 40, 60); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; @@ -640,7 +667,7 @@ mod tests { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); let (_, code, _) = compiled.into_parts_or_default(); let mut libs = vec![]; - let runner = runner(compiled.abi.as_ref().unwrap(), code, &mut libs); + let runner = runner(compiled.abi.as_ref().unwrap(), code, &mut libs, 40, 60); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; @@ -658,7 +685,7 @@ mod tests { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); let (_, code, _) = compiled.into_parts_or_default(); let mut libs = vec![]; - let runner = runner(compiled.abi.as_ref().unwrap(), code, &mut libs); + let runner = runner(compiled.abi.as_ref().unwrap(), code, &mut libs, 40, 60); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; @@ -674,7 +701,7 @@ mod tests { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); let (_, code, _) = compiled.into_parts_or_default(); let mut libs = vec![]; - let runner = runner(compiled.abi.as_ref().unwrap(), code, &mut libs); + let runner = runner(compiled.abi.as_ref().unwrap(), code, &mut libs, 0, 100); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; @@ -711,7 +738,7 @@ mod tests { pub fn test_runner(compiled: CompactContractRef) { let (_, code, _) = compiled.into_parts_or_default(); let mut libs = vec![]; - let runner = sputnik::runner(compiled.abi.as_ref().unwrap(), code, &mut libs); + let runner = sputnik::runner(compiled.abi.as_ref().unwrap(), code, &mut libs, 40, 60); let res = runner.run_tests(&Filter::matches_all(), None, None).unwrap(); assert!(!res.is_empty()); From 1573e401bfa15701960bc1fb1495d970935ec7fa Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 8 Mar 2022 15:17:48 -0500 Subject: [PATCH 08/14] refactor --- evm-adapters/src/fuzz.rs | 206 ++---------------- .../src/fuzz_strategies/calldata_strategy.rs | 194 +++++++++++++++++ evm-adapters/src/fuzz_strategies/mod.rs | 2 + .../uint_strategy.rs} | 0 evm-adapters/src/lib.rs | 2 + .../cheatcodes/memory_stackstate_owned.rs | 43 +++- 6 files changed, 250 insertions(+), 197 deletions(-) create mode 100644 evm-adapters/src/fuzz_strategies/calldata_strategy.rs create mode 100644 evm-adapters/src/fuzz_strategies/mod.rs rename evm-adapters/src/{fuzz/strategies.rs => fuzz_strategies/uint_strategy.rs} (100%) diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 3796634b9e63..99049630320d 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -1,12 +1,14 @@ //! Fuzzing support abstracted over the [`Evm`](crate::Evm) used -use crate::{Evm, ASSUME_MAGIC_RETURN_CODE}; +use crate::{ + fuzz_strategies::calldata_strategy::fuzz_state_calldata, Evm, ASSUME_MAGIC_RETURN_CODE, +}; use ethers::{ - abi::{Abi, Function, ParamType, Token, Tokenizable}, - types::{Address, Bytes, I256, U256}, + abi::{Abi, Function}, + types::{Address, Bytes}, }; use std::{ cell::{RefCell, RefMut}, - collections::HashSet, + io::Write, marker::PhantomData, rc::Rc, }; @@ -18,8 +20,6 @@ use proptest::{ }; use serde::{Deserialize, Serialize}; -mod strategies; - /// Wrapper around any [`Evm`](crate::Evm) implementor which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/). /// /// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with @@ -164,6 +164,13 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { let new_flattened = evm.flatten_state(); let mut t = flattened_state.borrow_mut(); (*t).extend(new_flattened); + returndata.as_ref().chunks(32).for_each(|chunk| { + let mut to_fill: [u8; 32] = [0; 32]; + let _ = (&mut to_fill[..]) + .write(chunk) + .expect("Chunk cannot be greater than 32 bytes"); + (*t).insert(to_fill); + }); } // push test case to the case set @@ -275,193 +282,6 @@ pub struct FuzzCase { pub gas: u64, } -/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata -/// for that function's input types. -pub fn fuzz_state_calldata( - func: Function, - state: Option>>>, -) -> impl Strategy { - // We need to compose all the strategies generated for each parameter in all - // possible combinations - // let strategy = proptest::sample::select(state.clone().into_iter().collect::>()); - let strats = func - .inputs - .iter() - .map(|input| fuzz_param_with_input(&input.kind, state.clone())) - .collect::>(); - - strats.prop_map(move |tokens| { - tracing::trace!(input = ?tokens); - func.encode_input(&tokens).unwrap().into() - }) -} - -/// The max length of arrays we fuzz for is 256. -const MAX_ARRAY_LEN: usize = 256; - -fn fuzz_param_with_input( - param: &ParamType, - state: Option>>>, -) -> impl Strategy { - use proptest::prelude::*; - // The key to making this work is the `boxed()` call which type erases everything - // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html - if let Some(state) = state { - let selectors = any::(); - match param { - ParamType::Address => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Address::from_slice(&x[12..]).into_token() - }) - .boxed(), - ParamType::Bytes => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Bytes::from(x).into_token() - }) - .boxed(), - ParamType::Int(n) => match n / 8 { - 32 => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - I256::from_raw(U256::from(x)).into_token() - }) - .boxed(), - y @ 1..=31 => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - // Generate a uintN in the correct range, then shift it to the range of intN - // by subtracting 2^(N-1) - let uint = U256::from(x) % U256::from(2).pow(U256::from(y * 8)); - let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); - let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); - num.into_token() - }) - .boxed(), - _ => panic!("unsupported solidity type int{}", n), - }, - ParamType::Uint(n) => match n / 8 { - 32 => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - U256::from(x).into_token() - }) - .boxed(), - y @ 1..=31 => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - (U256::from(x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() - }) - .boxed(), - _ => panic!("unsupported solidity type uint{}", n), - }, - ParamType::Bool => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Token::Bool(x[31] == 1) - }) - .boxed(), - ParamType::String => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) - }) - .boxed(), - ParamType::Array(param) => proptest::collection::vec( - fuzz_param_with_input(param, Some(state)), - 0..MAX_ARRAY_LEN, - ) - .prop_map(Token::Array) - .boxed(), - ParamType::FixedBytes(size) => { - // we have to clone outside the prop_map to satisfy lifetime constraints - let v = *size; - selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - let val = x[32 - v..].to_vec(); - Token::FixedBytes(val) - }) - .boxed() - } - ParamType::FixedArray(param, size) => (0..*size as u64) - .map(|_| { - fuzz_param_with_input(param, Some(state.clone())) - .prop_map(|param| param.into_token()) - }) - .collect::>() - .prop_map(Token::FixedArray) - .boxed(), - ParamType::Tuple(params) => params - .iter() - .map(|p| fuzz_param_with_input(p, Some(state.clone()))) - .collect::>() - .prop_map(Token::Tuple) - .boxed(), - } - } else { - match param { - ParamType::Address => { - any::<[u8; 20]>().prop_map(|x| Address::from_slice(&x).into_token()).boxed() - } - ParamType::Bytes => any::>().prop_map(|x| Bytes::from(x).into_token()).boxed(), - // For ints and uints we sample from a U256, then wrap it to the correct size with a - // modulo operation. Note that this introduces modulo bias, but it can be removed with - // rejection sampling if it's determined the bias is too severe. Rejection sampling may - // slow down tests as it resamples bad values, so may want to benchmark the performance - // hit and weigh that against the current bias before implementing - ParamType::Int(n) => match n / 8 { - 32 => any::<[u8; 32]>() - .prop_map(move |x| I256::from_raw(U256::from(&x)).into_token()) - .boxed(), - y @ 1..=31 => any::<[u8; 32]>() - .prop_map(move |x| { - // Generate a uintN in the correct range, then shift it to the range of intN - // by subtracting 2^(N-1) - let uint = U256::from(&x) % U256::from(2).pow(U256::from(y * 8)); - let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); - let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); - num.into_token() - }) - .boxed(), - _ => panic!("unsupported solidity type int{}", n), - }, - ParamType::Uint(n) => { - strategies::UintStrategy::new(*n, vec![]).prop_map(|x| x.into_token()).boxed() - } - ParamType::Bool => any::().prop_map(|x| x.into_token()).boxed(), - ParamType::String => any::>() - .prop_map(|x| { - Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) - }) - .boxed(), - ParamType::Array(param) => { - proptest::collection::vec(fuzz_param_with_input(param, None), 0..MAX_ARRAY_LEN) - .prop_map(Token::Array) - .boxed() - } - ParamType::FixedBytes(size) => (0..*size as u64) - .map(|_| any::()) - .collect::>() - .prop_map(Token::FixedBytes) - .boxed(), - ParamType::FixedArray(param, size) => (0..*size as u64) - .map(|_| fuzz_param_with_input(param, None).prop_map(|param| param.into_token())) - .collect::>() - .prop_map(Token::FixedArray) - .boxed(), - ParamType::Tuple(params) => params - .iter() - .map(|p| fuzz_param_with_input(p, None)) - .collect::>() - .prop_map(Token::Tuple) - .boxed(), - } - } -} - #[cfg(test)] #[cfg(feature = "sputnik")] mod tests { diff --git a/evm-adapters/src/fuzz_strategies/calldata_strategy.rs b/evm-adapters/src/fuzz_strategies/calldata_strategy.rs new file mode 100644 index 000000000000..b45701f966b8 --- /dev/null +++ b/evm-adapters/src/fuzz_strategies/calldata_strategy.rs @@ -0,0 +1,194 @@ +use crate::fuzz_strategies::uint_strategy::*; +use ethers::{ + abi::{Function, ParamType, Token, Tokenizable}, + types::{Address, Bytes, I256, U256}, +}; +use proptest::prelude::Strategy; +use std::{cell::RefCell, collections::HashSet, rc::Rc}; + +/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata +/// for that function's input types. +pub fn fuzz_state_calldata( + func: Function, + state: Option>>>, +) -> impl Strategy { + // We need to compose all the strategies generated for each parameter in all + // possible combinations + // let strategy = proptest::sample::select(state.clone().into_iter().collect::>()); + let strats = func + .inputs + .iter() + .map(|input| fuzz_param_with_input(&input.kind, state.clone())) + .collect::>(); + + strats.prop_map(move |tokens| { + tracing::trace!(input = ?tokens); + func.encode_input(&tokens).unwrap().into() + }) +} + +/// The max length of arrays we fuzz for is 256. +const MAX_ARRAY_LEN: usize = 256; + +fn fuzz_param_with_input( + param: &ParamType, + state: Option>>>, +) -> impl Strategy { + use proptest::prelude::*; + // The key to making this work is the `boxed()` call which type erases everything + // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html + if let Some(state) = state { + let selectors = any::(); + match param { + ParamType::Address => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Address::from_slice(&x[12..]).into_token() + }) + .boxed(), + ParamType::Bytes => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Bytes::from(x).into_token() + }) + .boxed(), + ParamType::Int(n) => match n / 8 { + 32 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + I256::from_raw(U256::from(x)).into_token() + }) + .boxed(), + y @ 1..=31 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + // Generate a uintN in the correct range, then shift it to the range of intN + // by subtracting 2^(N-1) + let uint = U256::from(x) % U256::from(2).pow(U256::from(y * 8)); + let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); + let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); + num.into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type int{}", n), + }, + ParamType::Uint(n) => match n / 8 { + 32 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + U256::from(x).into_token() + }) + .boxed(), + y @ 1..=31 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + (U256::from(x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type uint{}", n), + }, + ParamType::Bool => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Token::Bool(x[31] == 1) + }) + .boxed(), + ParamType::String => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) + }) + .boxed(), + ParamType::Array(param) => proptest::collection::vec( + fuzz_param_with_input(param, Some(state)), + 0..MAX_ARRAY_LEN, + ) + .prop_map(Token::Array) + .boxed(), + ParamType::FixedBytes(size) => { + // we have to clone outside the prop_map to satisfy lifetime constraints + let v = *size; + selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + let val = x[32 - v..].to_vec(); + Token::FixedBytes(val) + }) + .boxed() + } + ParamType::FixedArray(param, size) => (0..*size as u64) + .map(|_| { + fuzz_param_with_input(param, Some(state.clone())) + .prop_map(|param| param.into_token()) + }) + .collect::>() + .prop_map(Token::FixedArray) + .boxed(), + ParamType::Tuple(params) => params + .iter() + .map(|p| fuzz_param_with_input(p, Some(state.clone()))) + .collect::>() + .prop_map(Token::Tuple) + .boxed(), + } + } else { + match param { + ParamType::Address => { + any::<[u8; 20]>().prop_map(|x| Address::from_slice(&x).into_token()).boxed() + } + ParamType::Bytes => any::>().prop_map(|x| Bytes::from(x).into_token()).boxed(), + // For ints and uints we sample from a U256, then wrap it to the correct size with a + // modulo operation. Note that this introduces modulo bias, but it can be removed with + // rejection sampling if it's determined the bias is too severe. Rejection sampling may + // slow down tests as it resamples bad values, so may want to benchmark the performance + // hit and weigh that against the current bias before implementing + ParamType::Int(n) => match n / 8 { + 32 => any::<[u8; 32]>() + .prop_map(move |x| I256::from_raw(U256::from(&x)).into_token()) + .boxed(), + y @ 1..=31 => any::<[u8; 32]>() + .prop_map(move |x| { + // Generate a uintN in the correct range, then shift it to the range of intN + // by subtracting 2^(N-1) + let uint = U256::from(&x) % U256::from(2).pow(U256::from(y * 8)); + let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); + let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); + num.into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type int{}", n), + }, + ParamType::Uint(n) => { + UintStrategy::new(*n, vec![]).prop_map(|x| x.into_token()).boxed() + } + ParamType::Bool => any::().prop_map(|x| x.into_token()).boxed(), + ParamType::String => any::>() + .prop_map(|x| { + Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) + }) + .boxed(), + ParamType::Array(param) => { + proptest::collection::vec(fuzz_param_with_input(param, None), 0..MAX_ARRAY_LEN) + .prop_map(Token::Array) + .boxed() + } + ParamType::FixedBytes(size) => (0..*size as u64) + .map(|_| any::()) + .collect::>() + .prop_map(Token::FixedBytes) + .boxed(), + ParamType::FixedArray(param, size) => (0..*size as u64) + .map(|_| fuzz_param_with_input(param, None).prop_map(|param| param.into_token())) + .collect::>() + .prop_map(Token::FixedArray) + .boxed(), + ParamType::Tuple(params) => params + .iter() + .map(|p| fuzz_param_with_input(p, None)) + .collect::>() + .prop_map(Token::Tuple) + .boxed(), + } + } +} diff --git a/evm-adapters/src/fuzz_strategies/mod.rs b/evm-adapters/src/fuzz_strategies/mod.rs new file mode 100644 index 000000000000..4b65de0854eb --- /dev/null +++ b/evm-adapters/src/fuzz_strategies/mod.rs @@ -0,0 +1,2 @@ +pub mod calldata_strategy; +pub mod uint_strategy; diff --git a/evm-adapters/src/fuzz/strategies.rs b/evm-adapters/src/fuzz_strategies/uint_strategy.rs similarity index 100% rename from evm-adapters/src/fuzz/strategies.rs rename to evm-adapters/src/fuzz_strategies/uint_strategy.rs diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index a4cf310ec6ee..1eaa3791f80c 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -20,6 +20,8 @@ pub mod gas_report; /// Helpers for easily constructing EVM objects. pub mod evm_opts; +pub mod fuzz_strategies; + use ethers::{ abi::{Abi, Detokenize, Tokenize}, contract::{decode_function_data, encode_function_data}, diff --git a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs index eeed2f8d5776..b1b173f4d5bf 100644 --- a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs +++ b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs @@ -1,10 +1,14 @@ use sputnik::{ backend::{Apply, Backend, Basic}, executor::stack::{MemoryStackSubstate, StackState, StackSubstateMetadata}, - ExitError, Transfer, + ExitError, Opcode, Transfer, }; -use crate::{call_tracing::CallTraceArena, sputnik::cheatcodes::debugger::DebugArena, FuzzState}; +use crate::{ + call_tracing::CallTraceArena, + sputnik::cheatcodes::debugger::{DebugArena, OpCode}, + FuzzState, +}; use ethers::{ abi::RawLog, types::{H160, H256, U256}, @@ -155,12 +159,39 @@ impl<'config, B: Backend> MemoryStackStateOwned<'config, B> { impl<'config, B: Backend> FuzzState for MemoryStackStateOwned<'config, B> { fn flatten_state(&self) -> HashSet<[u8; 32]> { + use std::io::Write; + let mut flattened: HashSet<[u8; 32]> = HashSet::new(); let (applies, logs) = self.substate.clone().deconstruct(&self.backend); for apply in applies { - if let Apply::Modify { address, basic, storage, .. } = apply { + if let Apply::Modify { address, basic, storage, code, .. } = apply { let old_basic = self.basic(address); flattened.insert(H256::from(address).into()); + + // insert all push bytes + if let Some(code) = code { + let mut i = 0; + while i < code.len() { + let wrapped_op = OpCode::from(Opcode(code[i])); + if let Some(push_size) = wrapped_op.push_size() { + let push_start = i + 1; + i = push_start + push_size as usize; + if i < code.len() { + let mut to_fill: [u8; 32] = [0; 32]; + let _ = (&mut to_fill[..]) + .write(&code[push_start..i]) + .expect("PUSH cannot be greater than 32 bytes"); + flattened.insert(to_fill); + } else { + // because of metadata bs, we may hit this codepath. just ignore it + // and continue on + } + } else { + i += 1; + } + } + } + // we keep bytes as little endian let mut h = H256::default(); old_basic.balance.to_little_endian(h.as_mut()); @@ -187,7 +218,11 @@ impl<'config, B: Backend> FuzzState for MemoryStackStateOwned<'config, B> { flattened.insert(topic.0); }); log.data.chunks(32).for_each(|chunk| { - flattened.insert(H256::from_slice(chunk).into()); + let mut to_fill: [u8; 32] = [0; 32]; + let _ = (&mut to_fill[..]) + .write(chunk) + .expect("Chunk cannot be greater than 32 bytes"); + flattened.insert(to_fill); }); } flattened From be3992f82b2021ce38c8b1fd1a3cec4a7e4e4f13 Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 8 Mar 2022 15:58:34 -0500 Subject: [PATCH 09/14] refactor --- .../src/fuzz_strategies/calldata_strategy.rs | 192 +++++++++--------- .../cheatcodes/memory_stackstate_owned.rs | 5 +- 2 files changed, 99 insertions(+), 98 deletions(-) diff --git a/evm-adapters/src/fuzz_strategies/calldata_strategy.rs b/evm-adapters/src/fuzz_strategies/calldata_strategy.rs index b45701f966b8..15e46a861352 100644 --- a/evm-adapters/src/fuzz_strategies/calldata_strategy.rs +++ b/evm-adapters/src/fuzz_strategies/calldata_strategy.rs @@ -3,7 +3,7 @@ use ethers::{ abi::{Function, ParamType, Token, Tokenizable}, types::{Address, Bytes, I256, U256}, }; -use proptest::prelude::Strategy; +use proptest::prelude::{Strategy, *}; use std::{cell::RefCell, collections::HashSet, rc::Rc}; /// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata @@ -35,103 +35,10 @@ fn fuzz_param_with_input( param: &ParamType, state: Option>>>, ) -> impl Strategy { - use proptest::prelude::*; // The key to making this work is the `boxed()` call which type erases everything // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html if let Some(state) = state { - let selectors = any::(); - match param { - ParamType::Address => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Address::from_slice(&x[12..]).into_token() - }) - .boxed(), - ParamType::Bytes => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Bytes::from(x).into_token() - }) - .boxed(), - ParamType::Int(n) => match n / 8 { - 32 => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - I256::from_raw(U256::from(x)).into_token() - }) - .boxed(), - y @ 1..=31 => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - // Generate a uintN in the correct range, then shift it to the range of intN - // by subtracting 2^(N-1) - let uint = U256::from(x) % U256::from(2).pow(U256::from(y * 8)); - let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); - let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); - num.into_token() - }) - .boxed(), - _ => panic!("unsupported solidity type int{}", n), - }, - ParamType::Uint(n) => match n / 8 { - 32 => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - U256::from(x).into_token() - }) - .boxed(), - y @ 1..=31 => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - (U256::from(x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() - }) - .boxed(), - _ => panic!("unsupported solidity type uint{}", n), - }, - ParamType::Bool => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Token::Bool(x[31] == 1) - }) - .boxed(), - ParamType::String => selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) - }) - .boxed(), - ParamType::Array(param) => proptest::collection::vec( - fuzz_param_with_input(param, Some(state)), - 0..MAX_ARRAY_LEN, - ) - .prop_map(Token::Array) - .boxed(), - ParamType::FixedBytes(size) => { - // we have to clone outside the prop_map to satisfy lifetime constraints - let v = *size; - selectors - .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - let val = x[32 - v..].to_vec(); - Token::FixedBytes(val) - }) - .boxed() - } - ParamType::FixedArray(param, size) => (0..*size as u64) - .map(|_| { - fuzz_param_with_input(param, Some(state.clone())) - .prop_map(|param| param.into_token()) - }) - .collect::>() - .prop_map(Token::FixedArray) - .boxed(), - ParamType::Tuple(params) => params - .iter() - .map(|p| fuzz_param_with_input(p, Some(state.clone()))) - .collect::>() - .prop_map(Token::Tuple) - .boxed(), - } + state_fuzz(param, state) } else { match param { ParamType::Address => { @@ -192,3 +99,98 @@ fn fuzz_param_with_input( } } } + +fn state_fuzz(param: &ParamType, state: Rc>>) -> BoxedStrategy { + let selectors = any::(); + match param { + ParamType::Address => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Address::from_slice(&x[12..]).into_token() + }) + .boxed(), + ParamType::Bytes => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Bytes::from(x).into_token() + }) + .boxed(), + ParamType::Int(n) => match n / 8 { + 32 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + I256::from_raw(U256::from(x)).into_token() + }) + .boxed(), + y @ 1..=31 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + // Generate a uintN in the correct range, then shift it to the range of intN + // by subtracting 2^(N-1) + let uint = U256::from(x) % U256::from(2).pow(U256::from(y * 8)); + let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); + let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); + num.into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type int{}", n), + }, + ParamType::Uint(n) => match n / 8 { + 32 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + U256::from(x).into_token() + }) + .boxed(), + y @ 1..=31 => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + (U256::from(x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type uint{}", n), + }, + ParamType::Bool => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Token::Bool(x[31] == 1) + }) + .boxed(), + ParamType::String => selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) + }) + .boxed(), + ParamType::Array(param) => { + proptest::collection::vec(fuzz_param_with_input(param, Some(state)), 0..MAX_ARRAY_LEN) + .prop_map(Token::Array) + .boxed() + } + ParamType::FixedBytes(size) => { + // we have to clone outside the prop_map to satisfy lifetime constraints + let v = *size; + selectors + .prop_map(move |selector| { + let x = *selector.select(&*state.borrow()); + let val = x[32 - v..].to_vec(); + Token::FixedBytes(val) + }) + .boxed() + } + ParamType::FixedArray(param, size) => (0..*size as u64) + .map(|_| { + fuzz_param_with_input(param, Some(state.clone())) + .prop_map(|param| param.into_token()) + }) + .collect::>() + .prop_map(Token::FixedArray) + .boxed(), + ParamType::Tuple(params) => params + .iter() + .map(|p| fuzz_param_with_input(p, Some(state.clone()))) + .collect::>() + .prop_map(Token::Tuple) + .boxed(), + } +} diff --git a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs index b1b173f4d5bf..69a89a92cb65 100644 --- a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs +++ b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs @@ -219,9 +219,8 @@ impl<'config, B: Backend> FuzzState for MemoryStackStateOwned<'config, B> { }); log.data.chunks(32).for_each(|chunk| { let mut to_fill: [u8; 32] = [0; 32]; - let _ = (&mut to_fill[..]) - .write(chunk) - .expect("Chunk cannot be greater than 32 bytes"); + let _ = + (&mut to_fill[..]).write(chunk).expect("Chunk cannot be greater than 32 bytes"); flattened.insert(to_fill); }); } From 211a47cb61d5e8157986033f03f679689c4a0c57 Mon Sep 17 00:00:00 2001 From: Brock Elmore Date: Thu, 10 Mar 2022 13:41:49 -0500 Subject: [PATCH 10/14] forge install: ds-test --- .gitmodules | 3 +++ foundry_seminar2/lib/ds-test | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 foundry_seminar2/lib/ds-test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..f1cc7c00db89 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "foundry_seminar2/lib/ds-test"] + path = foundry_seminar2/lib/ds-test + url = https://github.com/dapphub/ds-test diff --git a/foundry_seminar2/lib/ds-test b/foundry_seminar2/lib/ds-test new file mode 160000 index 000000000000..0a5da56b0d65 --- /dev/null +++ b/foundry_seminar2/lib/ds-test @@ -0,0 +1 @@ +Subproject commit 0a5da56b0d65960e6a994d2ec8245e6edd38c248 From f1f82553786d2c9936b36f4581a6a656e92bad5b Mon Sep 17 00:00:00 2001 From: Brock Elmore Date: Thu, 10 Mar 2022 13:43:26 -0500 Subject: [PATCH 11/14] forge install: solmate --- .gitmodules | 3 +++ foundry_seminar2/lib/solmate | 1 + 2 files changed, 4 insertions(+) create mode 160000 foundry_seminar2/lib/solmate diff --git a/.gitmodules b/.gitmodules index f1cc7c00db89..f98308e31c9d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "foundry_seminar2/lib/ds-test"] path = foundry_seminar2/lib/ds-test url = https://github.com/dapphub/ds-test +[submodule "foundry_seminar2/lib/solmate"] + path = foundry_seminar2/lib/solmate + url = https://github.com/Rari-Capital/solmate diff --git a/foundry_seminar2/lib/solmate b/foundry_seminar2/lib/solmate new file mode 160000 index 000000000000..e16334b8cf1d --- /dev/null +++ b/foundry_seminar2/lib/solmate @@ -0,0 +1 @@ +Subproject commit e16334b8cf1d58ded39ce63b5e60e78cc42aeb5b From 00ef394224dcbb924021bcea5d47ddadfa382ebc Mon Sep 17 00:00:00 2001 From: Brock Elmore Date: Thu, 10 Mar 2022 13:43:53 -0500 Subject: [PATCH 12/14] forge install: forge-std --- .gitmodules | 3 +++ foundry_seminar2/lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 160000 foundry_seminar2/lib/forge-std diff --git a/.gitmodules b/.gitmodules index f98308e31c9d..9c5992128401 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "foundry_seminar2/lib/solmate"] path = foundry_seminar2/lib/solmate url = https://github.com/Rari-Capital/solmate +[submodule "foundry_seminar2/lib/forge-std"] + path = foundry_seminar2/lib/forge-std + url = https://github.com/brockelmore/forge-std diff --git a/foundry_seminar2/lib/forge-std b/foundry_seminar2/lib/forge-std new file mode 160000 index 000000000000..018b7a57ac40 --- /dev/null +++ b/foundry_seminar2/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 018b7a57ac40c9e4d2b0076b3394e0ee54b7143f From bea6243b9cecc9430162d7e6ca43cccb894f41e8 Mon Sep 17 00:00:00 2001 From: Brock Date: Thu, 17 Mar 2022 16:55:33 -0400 Subject: [PATCH 13/14] updates --- Cargo.lock | 1 + evm-adapters/Cargo.toml | 1 + evm-adapters/src/fuzz.rs | 23 +++- .../src/fuzz_strategies/calldata_strategy.rs | 27 ++--- evm-adapters/src/fuzz_strategies/mod.rs | 1 + .../src/fuzz_strategies/random_selector.rs | 108 ++++++++++++++++++ evm-adapters/src/lib.rs | 3 +- .../sputnik/cheatcodes/cheatcode_handler.rs | 4 +- .../cheatcodes/memory_stackstate_owned.rs | 6 +- evm-adapters/src/sputnik/evm.rs | 5 +- evm-adapters/src/sputnik/mod.rs | 5 +- 11 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 evm-adapters/src/fuzz_strategies/random_selector.rs diff --git a/Cargo.lock b/Cargo.lock index 14ddb8838625..abec58816d43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1519,6 +1519,7 @@ dependencies = [ "ethers-core", "evm", "eyre", + "fnv", "foundry-utils", "futures", "hex", diff --git a/evm-adapters/Cargo.toml b/evm-adapters/Cargo.toml index 82dd081782fb..cd1c6491f28e 100644 --- a/evm-adapters/Cargo.toml +++ b/evm-adapters/Cargo.toml @@ -28,6 +28,7 @@ serde_json = "1.0.72" serde = "1.0.130" ansi_term = "0.12.1" comfy-table = "5.0.0" +fnv = "1.0.3" [dev-dependencies] ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "solc-tests"] } diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 99049630320d..f536af59a08a 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -1,4 +1,5 @@ //! Fuzzing support abstracted over the [`Evm`](crate::Evm) used + use crate::{ fuzz_strategies::calldata_strategy::fuzz_state_calldata, Evm, ASSUME_MAGIC_RETURN_CODE, }; @@ -88,7 +89,6 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { // Snapshot the state before the test starts running let pre_test_state = self.evm.borrow().state().clone(); - let flattened_state = Rc::new(RefCell::new(self.evm.borrow().flatten_state())); // we dont shrink for state strategy @@ -117,6 +117,8 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { let mut runner = self.runner.clone(); tracing::debug!(func = ?func.name, should_fail, "fuzzing"); + let ret_calldata = RefCell::new(None); + let test_error = runner .run(&combined_strat, |calldata| { let mut evm = self.evm.borrow_mut(); @@ -146,6 +148,13 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { let revert = foundry_utils::decode_revert(returndata.as_ref(), abi).unwrap_or_default(); let _ = revert_reason.borrow_mut().insert(revert); + + // because of how we do state selector, (totally random) + // we have to manually set the test_error data. Otherwise + // the way proptest works, makes it so the failing calldata wouldnt be the same + // as the test_error calldata. so we do this instead + let mut cd = ret_calldata.borrow_mut(); + *cd = Some(calldata.clone()); } // This will panic and get caught by the executor @@ -161,9 +170,9 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { ); { - let new_flattened = evm.flatten_state(); let mut t = flattened_state.borrow_mut(); - (*t).extend(new_flattened); + (*t).extend(evm.flatten_state()); + returndata.as_ref().chunks(32).for_each(|chunk| { let mut to_fill: [u8; 32] = [0; 32]; let _ = (&mut to_fill[..]) @@ -180,7 +189,13 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { }) .err() .map(|test_error| FuzzError { - test_error, + // selector strategy isnt reproducible, so we hack around that by using a refcell + test_error: match test_error { + TestError::Abort(msg) => TestError::Abort(msg), + TestError::Fail(msg, _cd) => { + TestError::Fail(msg, ret_calldata.into_inner().expect("Calldata must be set")) + } + }, return_reason: return_reason.into_inner().expect("Reason must be set"), revert_reason: revert_reason.into_inner().expect("Revert error string must be set"), }); diff --git a/evm-adapters/src/fuzz_strategies/calldata_strategy.rs b/evm-adapters/src/fuzz_strategies/calldata_strategy.rs index 15e46a861352..72befd0bf52b 100644 --- a/evm-adapters/src/fuzz_strategies/calldata_strategy.rs +++ b/evm-adapters/src/fuzz_strategies/calldata_strategy.rs @@ -1,11 +1,12 @@ -use crate::fuzz_strategies::uint_strategy::*; +use crate::fuzz_strategies::{random_selector::*, uint_strategy::*}; use ethers::{ abi::{Function, ParamType, Token, Tokenizable}, types::{Address, Bytes, I256, U256}, }; use proptest::prelude::{Strategy, *}; -use std::{cell::RefCell, collections::HashSet, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; +use fnv::{FnvHashSet as HashSet}; /// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata /// for that function's input types. pub fn fuzz_state_calldata( @@ -101,30 +102,30 @@ fn fuzz_param_with_input( } fn state_fuzz(param: &ParamType, state: Rc>>) -> BoxedStrategy { - let selectors = any::(); + let selectors = any::(); match param { ParamType::Address => selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); + let x = *selector.select(state.borrow().iter()); Address::from_slice(&x[12..]).into_token() }) .boxed(), ParamType::Bytes => selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); + let x = *selector.select(state.borrow().iter()); Bytes::from(x).into_token() }) .boxed(), ParamType::Int(n) => match n / 8 { 32 => selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); + let x = *selector.select(state.borrow().iter()); I256::from_raw(U256::from(x)).into_token() }) .boxed(), y @ 1..=31 => selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); + let x = *selector.select(state.borrow().iter()); // Generate a uintN in the correct range, then shift it to the range of intN // by subtracting 2^(N-1) let uint = U256::from(x) % U256::from(2).pow(U256::from(y * 8)); @@ -138,13 +139,13 @@ fn state_fuzz(param: &ParamType, state: Rc>>) -> Boxed ParamType::Uint(n) => match n / 8 { 32 => selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); + let x = *selector.select(state.borrow().iter()); U256::from(x).into_token() }) .boxed(), y @ 1..=31 => selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); + let x = *selector.select(state.borrow().iter()); (U256::from(x) % (U256::from(2).pow(U256::from(y * 8)))).into_token() }) .boxed(), @@ -152,14 +153,14 @@ fn state_fuzz(param: &ParamType, state: Rc>>) -> Boxed }, ParamType::Bool => selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); + let x = *selector.select(state.borrow().iter()); Token::Bool(x[31] == 1) }) .boxed(), ParamType::String => selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); - Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() }) + let x = *selector.select(state.borrow().iter()); + Token::String(unsafe { std::str::from_utf8_unchecked(&x[..]).to_string() }) }) .boxed(), ParamType::Array(param) => { @@ -172,7 +173,7 @@ fn state_fuzz(param: &ParamType, state: Rc>>) -> Boxed let v = *size; selectors .prop_map(move |selector| { - let x = *selector.select(&*state.borrow()); + let x = *selector.select(state.borrow().iter()); let val = x[32 - v..].to_vec(); Token::FixedBytes(val) }) diff --git a/evm-adapters/src/fuzz_strategies/mod.rs b/evm-adapters/src/fuzz_strategies/mod.rs index 4b65de0854eb..75ae2f78bfe4 100644 --- a/evm-adapters/src/fuzz_strategies/mod.rs +++ b/evm-adapters/src/fuzz_strategies/mod.rs @@ -1,2 +1,3 @@ pub mod calldata_strategy; pub mod uint_strategy; +pub mod random_selector; \ No newline at end of file diff --git a/evm-adapters/src/fuzz_strategies/random_selector.rs b/evm-adapters/src/fuzz_strategies/random_selector.rs new file mode 100644 index 000000000000..6aad9e43ceb9 --- /dev/null +++ b/evm-adapters/src/fuzz_strategies/random_selector.rs @@ -0,0 +1,108 @@ +use ethers_core::rand::prelude::IteratorRandom; +use proptest::prelude::Arbitrary; +use proptest::test_runner::TestRng; +use proptest::num::u64::BinarySearch; + +use proptest::{ + strategy::{NewTree, Strategy, ValueTree}, + test_runner::TestRunner, +}; + +#[derive(Clone, Debug)] +pub struct Selector { + rng: TestRng, +} + +/// Strategy to create `Selector`s. +/// +/// Created via `any::()`. +#[derive(Debug)] +pub struct SelectorStrategy { + _nonexhaustive: (), +} + +/// `ValueTree` corresponding to `SelectorStrategy`. +#[derive(Debug)] +pub struct SelectorValueTree { + rng: TestRng, + reverse_bias_increment: BinarySearch, +} + +impl SelectorStrategy { + pub(crate) fn new() -> Self { + SelectorStrategy { _nonexhaustive: () } + } +} + +impl Strategy for SelectorStrategy { + type Tree = SelectorValueTree; + type Value = Selector; + + fn new_tree(&self, runner: &mut TestRunner) -> NewTree { + Ok(SelectorValueTree { + rng: runner.new_rng(), + reverse_bias_increment: BinarySearch::new(u64::MAX), + }) + } +} + +impl ValueTree for SelectorValueTree { + type Value = Selector; + + fn current(&self) -> Selector { + Selector { + rng: self.rng.clone() + } + } + + fn simplify(&mut self) -> bool { + self.reverse_bias_increment.simplify() + } + + fn complicate(&mut self) -> bool { + self.reverse_bias_increment.complicate() + } +} + +impl Selector { + /// Pick a random element from iterable `it`. + /// + /// The selection is unaffected by the elements themselves, and is + /// dependent only on the actual length of `it`. + /// + /// `it` is always iterated completely. + /// + /// ## Panics + /// + /// Panics if `it` has no elements. + pub fn select<'a, T: IteratorRandom>(&self, it: T) -> ::Item + where + T: Iterator + { + self.try_select(it).expect("select from empty iterator") + } + + /// Pick a random element from iterable `it`. + /// + /// Returns `None` if `it` is empty. + /// + /// The selection is unaffected by the elements themselves, and is + /// dependent only on the actual length of `it`. + pub fn try_select<'a, T: IteratorRandom>(&self, it: T) -> Option<::Item> + where + T: Iterator + { + let mut rng = self.rng.clone(); + it.choose(&mut rng) + } +} + +impl Arbitrary for Selector { + type Parameters = (); + + type Strategy = SelectorStrategy; + + fn arbitrary_with(_: ()) -> SelectorStrategy { + SelectorStrategy::new() + } +} \ No newline at end of file diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index 1eaa3791f80c..d16b5fb8c4dc 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -7,7 +7,8 @@ use crate::sputnik::cheatcodes::debugger::DebugArena; mod blocking_provider; use crate::call_tracing::CallTraceArena; -use std::collections::HashSet; + +use fnv::{FnvHashSet as HashSet}; pub use blocking_provider::BlockingProvider; diff --git a/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs b/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs index 581054812be4..d327b0f7b9cb 100644 --- a/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs +++ b/evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs @@ -8,7 +8,9 @@ use crate::{ sputnik::{cheatcodes::memory_stackstate_owned::ExpectedEmit, Executor, SputnikExecutor}, Evm, ASSUME_MAGIC_RETURN_CODE, }; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap}; + +use fnv::{FnvHashSet as HashSet}; use std::{fs::File, io::Read, path::Path}; diff --git a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs index 69a89a92cb65..3fef50a8f0e9 100644 --- a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs +++ b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs @@ -16,10 +16,12 @@ use ethers::{ use std::{ cell::RefCell, - collections::{BTreeMap, HashSet}, + collections::{BTreeMap}, rc::Rc, }; +use fnv::{FnvHashSet as HashSet}; + #[derive(Clone, Default)] pub struct RecordAccess { pub reads: RefCell>>, @@ -161,7 +163,7 @@ impl<'config, B: Backend> FuzzState for MemoryStackStateOwned<'config, B> { fn flatten_state(&self) -> HashSet<[u8; 32]> { use std::io::Write; - let mut flattened: HashSet<[u8; 32]> = HashSet::new(); + let mut flattened = HashSet::default(); let (applies, logs) = self.substate.clone().deconstruct(&self.backend); for apply in applies { if let Apply::Modify { address, basic, storage, code, .. } = apply { diff --git a/evm-adapters/src/sputnik/evm.rs b/evm-adapters/src/sputnik/evm.rs index ca5050fdaabc..466dbabde459 100644 --- a/evm-adapters/src/sputnik/evm.rs +++ b/evm-adapters/src/sputnik/evm.rs @@ -11,10 +11,13 @@ use sputnik::{ Config, CreateScheme, ExitReason, ExitRevert, Transfer, }; use std::{ - collections::{BTreeMap, HashSet}, + collections::{BTreeMap}, marker::PhantomData, }; + +use fnv::{FnvHashSet as HashSet}; + use eyre::Result; use super::SputnikExecutor; diff --git a/evm-adapters/src/sputnik/mod.rs b/evm-adapters/src/sputnik/mod.rs index b6601e620c4d..5dca716f729e 100644 --- a/evm-adapters/src/sputnik/mod.rs +++ b/evm-adapters/src/sputnik/mod.rs @@ -23,7 +23,8 @@ use crate::{call_tracing::CallTraceArena, sputnik::cheatcodes::debugger::DebugAr pub use sputnik as sputnik_evm; use sputnik_evm::executor::stack::PrecompileSet; -use std::collections::HashSet; + +use fnv::{FnvHashSet as HashSet}; /// Given an ethers provider and a block, it proceeds to construct a [`MemoryVicinity`] from /// the live chain data returned by the provider. @@ -204,7 +205,7 @@ impl<'a, 'b, S: StackState<'a>, P: PrecompileSet> SputnikExecutor fn clear_logs(&mut self) {} fn flatten_state(&self) -> HashSet<[u8; 32]> { - HashSet::new() + HashSet::default() } } From 58940a02d21c13fc27c7ba03ad1b877aa5c0a4d2 Mon Sep 17 00:00:00 2001 From: Brock Date: Thu, 17 Mar 2022 17:42:17 -0400 Subject: [PATCH 14/14] nit --- evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs index 3fef50a8f0e9..12da9cfe9935 100644 --- a/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs +++ b/evm-adapters/src/sputnik/cheatcodes/memory_stackstate_owned.rs @@ -184,9 +184,6 @@ impl<'config, B: Backend> FuzzState for MemoryStackStateOwned<'config, B> { .write(&code[push_start..i]) .expect("PUSH cannot be greater than 32 bytes"); flattened.insert(to_fill); - } else { - // because of metadata bs, we may hit this codepath. just ignore it - // and continue on } } else { i += 1;