diff --git a/Cargo.lock b/Cargo.lock index 4713a97590f4..96922afce358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -550,6 +565,7 @@ dependencies = [ "eyre", "glob", "hex", + "proptest", "regex", "semver 1.0.4", "serde", @@ -596,6 +612,7 @@ dependencies = [ "evm-adapters", "evmodin", "eyre", + "proptest", "regex", "rpassword", "rustc-hex", @@ -1005,7 +1022,7 @@ dependencies = [ [[package]] name = "evm" version = "0.30.1" -source = "git+https://github.com/gakonst/evm#5c338f1d6e412b7b0304481a23cc7517682a2f25" +source = "git+https://github.com/gakonst/evm?branch=feat/clone-debug#33a3d5288e8a105c7d1e2b5385937e1d96e6f678" dependencies = [ "environmental", "ethereum", @@ -1039,7 +1056,7 @@ dependencies = [ [[package]] name = "evm-core" version = "0.30.0" -source = "git+https://github.com/gakonst/evm#5c338f1d6e412b7b0304481a23cc7517682a2f25" +source = "git+https://github.com/gakonst/evm?branch=feat/clone-debug#33a3d5288e8a105c7d1e2b5385937e1d96e6f678" dependencies = [ "funty", "parity-scale-codec", @@ -1050,7 +1067,7 @@ dependencies = [ [[package]] name = "evm-gasometer" version = "0.30.0" -source = "git+https://github.com/gakonst/evm#5c338f1d6e412b7b0304481a23cc7517682a2f25" +source = "git+https://github.com/gakonst/evm?branch=feat/clone-debug#33a3d5288e8a105c7d1e2b5385937e1d96e6f678" dependencies = [ "environmental", "evm-core", @@ -1061,7 +1078,7 @@ dependencies = [ [[package]] name = "evm-runtime" version = "0.30.0" -source = "git+https://github.com/gakonst/evm#5c338f1d6e412b7b0304481a23cc7517682a2f25" +source = "git+https://github.com/gakonst/evm?branch=feat/clone-debug#33a3d5288e8a105c7d1e2b5385937e1d96e6f678" dependencies = [ "environmental", "evm-core", @@ -1072,7 +1089,7 @@ dependencies = [ [[package]] name = "evmodin" version = "0.1.0" -source = "git+https://github.com/vorot93/evmodin#ef1bee33aeee962421e32bf23cfb0bb4903a3125" +source = "git+https://github.com/gakonst/evmodin?branch=feat/clone-debug#15a35271f4e7fda41d88cc78216d4900ea03bace" dependencies = [ "arrayvec", "bytes", @@ -2152,6 +2169,38 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "proptest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error 2.0.1", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.9" @@ -2213,6 +2262,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "redox_syscall" version = "0.2.10" @@ -2399,6 +2457,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.5" @@ -3296,6 +3366,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 6d2f8ba297fc..fa945c7dba8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,9 @@ opt-level = "z" lto = true codegen-units = 1 panic = "abort" + +[patch."https://github.com/rust-blockchain/evm"] +evm = { git = "https://github.com/gakonst/evm", branch = "feat/clone-debug" } + +[patch."https://github.com/vorot93/evmodin"] +evmodin = { git = "https://github.com/gakonst/evmodin", branch = "feat/clone-debug" } diff --git a/dapp/Cargo.toml b/dapp/Cargo.toml index 890a8a4e1e71..cfe88c8a7ca6 100644 --- a/dapp/Cargo.toml +++ b/dapp/Cargo.toml @@ -26,9 +26,10 @@ glob = "0.3.0" tokio = { version = "1.10.1" } tracing = "0.1.26" tracing-subscriber = "0.2.20" +proptest = "1.0.0" [dev-dependencies] evm-adapters = { path = "./../evm-adapters", features = ["sputnik", "sputnik-helpers", "evmodin", "evmodin-helpers"] } evmodin = { git = "https://github.com/vorot93/evmodin", features = ["util"] } # evm = { version = "0.30.1" } -evm = { git = "https://github.com/gakonst/evm" } +evm = { git = "https://github.com/rust-blockchain/evm" } diff --git a/dapp/GreetTest.sol b/dapp/GreetTest.sol index 03097ee72f6c..2d93f6d8194f 100644 --- a/dapp/GreetTest.sol +++ b/dapp/GreetTest.sol @@ -30,6 +30,15 @@ contract GreeterTest is GreeterTestSetup { greeter.greet(greeting); } + function testFuzzing(string memory myGreeting) public { + greeter.greet(myGreeting); + require(keccak256(abi.encodePacked(greeter.greeting())) == keccak256(abi.encodePacked(myGreeting)), "not equal"); + } + + function testFuzzShrinking(uint256 x, uint256 y) public { + require(x * y <= 100, "product greater than 100"); + } + // check the positive case function testGreeting() public { greeter.greet("yo"); diff --git a/dapp/src/fuzz.rs b/dapp/src/fuzz.rs new file mode 100644 index 000000000000..d751d16d1558 --- /dev/null +++ b/dapp/src/fuzz.rs @@ -0,0 +1,37 @@ +use ethers::{ + abi::{Function, ParamType, Token, Tokenizable}, + types::{Address, Bytes, U256}, +}; + +use proptest::prelude::*; + +pub fn fuzz_calldata(func: &Function) -> 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::>(); + + strats.prop_map(move |tokens| func.encode_input(&tokens).unwrap().into()) +} + +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::Uint(n) => match n / 8 { + 1 => any::().prop_map(|x| x.into_token()).boxed(), + 2 => any::().prop_map(|x| x.into_token()).boxed(), + 3..=4 => any::().prop_map(|x| x.into_token()).boxed(), + 5..=8 => any::().prop_map(|x| x.into_token()).boxed(), + 9..=16 => any::().prop_map(|x| x.into_token()).boxed(), + 17..=32 => any::<[u8; 32]>().prop_map(|x| U256::from(&x).into_token()).boxed(), + _ => panic!("unsupported solidity type uint{}", n), + }, + ParamType::String => any::().prop_map(|x| x.into_token()).boxed(), + ParamType::Bytes => any::>().prop_map(|x| Bytes::from(x).into_token()).boxed(), + // TODO: Implement the rest of the strategies + _ => unimplemented!(), + } +} diff --git a/dapp/src/lib.rs b/dapp/src/lib.rs index 6d4924e77fa8..639bcec45d1e 100644 --- a/dapp/src/lib.rs +++ b/dapp/src/lib.rs @@ -7,6 +7,8 @@ pub use runner::{ContractRunner, TestResult}; mod multi_runner; pub use multi_runner::{MultiContractRunner, MultiContractRunnerBuilder}; +mod fuzz; + use ethers::abi; use eyre::Result; diff --git a/dapp/src/multi_runner.rs b/dapp/src/multi_runner.rs index 09d1f615224b..f4132ae28d8c 100644 --- a/dapp/src/multi_runner.rs +++ b/dapp/src/multi_runner.rs @@ -7,6 +7,7 @@ use ethers::{ utils::{keccak256, CompiledContract}, }; +use proptest::test_runner::TestRunner; use regex::Regex; use eyre::Result; @@ -24,12 +25,14 @@ pub struct MultiContractRunnerBuilder<'a> { /// The path for the output file pub out_path: PathBuf, pub no_compile: bool, + /// The fuzzer to be used for running fuzz tests + pub fuzzer: Option, } impl<'a> MultiContractRunnerBuilder<'a> { /// Given an EVM, proceeds to return a runner which is able to execute all tests /// against that evm - pub fn build(&self, mut evm: E) -> Result> + pub fn build(self, mut evm: E) -> Result> where E: Evm, { @@ -52,7 +55,13 @@ impl<'a> MultiContractRunnerBuilder<'a> { }); evm.initialize_contracts(init_state); - Ok(MultiContractRunner { contracts, addresses, evm, state: PhantomData }) + Ok(MultiContractRunner { + contracts, + addresses, + evm, + state: PhantomData, + fuzzer: self.fuzzer, + }) } pub fn contracts(mut self, contracts: &'a str) -> Self { @@ -60,6 +69,11 @@ impl<'a> MultiContractRunnerBuilder<'a> { self } + pub fn fuzzer(mut self, fuzzer: TestRunner) -> Self { + self.fuzzer = Some(fuzzer); + self + } + pub fn remappings(mut self, remappings: &'a [String]) -> Self { self.remappings = remappings; self @@ -88,12 +102,13 @@ pub struct MultiContractRunner { addresses: HashMap, /// The EVM instance used in the test runner evm: E, + fuzzer: Option, state: PhantomData, } impl MultiContractRunner where - E: Evm, + E: Evm + Clone, { pub fn test(&mut self, pattern: Regex) -> Result>> { // NB: We also have access to the contract's abi. When running the test. @@ -143,7 +158,7 @@ where pattern: &Regex, ) -> Result> { let mut runner = ContractRunner::new(&mut self.evm, contract, address); - runner.run_tests(pattern) + runner.run_tests(pattern, self.fuzzer.as_mut()) } } @@ -151,7 +166,7 @@ where mod tests { use super::*; - fn test_multi_runner>(evm: E) { + fn test_multi_runner>(evm: E) { let mut runner = MultiContractRunnerBuilder::default().contracts("./GreetTest.sol").build(evm).unwrap(); @@ -172,7 +187,7 @@ mod tests { assert_eq!(only_gm["GmTest"].len(), 1); } - fn test_ds_test_fail>(evm: E) { + fn test_ds_test_fail>(evm: E) { let mut runner = MultiContractRunnerBuilder::default().contracts("./../FooTest.sol").build(evm).unwrap(); let results = runner.test(Regex::new(".*").unwrap()).unwrap(); diff --git a/dapp/src/runner.rs b/dapp/src/runner.rs index 571cf32cad58..5feb9a688535 100644 --- a/dapp/src/runner.rs +++ b/dapp/src/runner.rs @@ -1,4 +1,9 @@ -use ethers::{abi::Function, types::Address, utils::CompiledContract}; +use ethers::{ + abi::{Function, Token}, + prelude::Bytes, + types::Address, + utils::CompiledContract, +}; use evm_adapters::Evm; @@ -6,13 +11,25 @@ use eyre::Result; use regex::Regex; use std::{collections::HashMap, time::Instant}; +use proptest::test_runner::{TestError, TestRunner}; use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CounterExample { + pub calldata: Bytes, + // Token does not implement Serde (lol), so we just serialize the calldata + #[serde(skip)] + pub args: Vec, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TestResult { pub success: bool, - // TODO: Ensure that this is calculated properly - pub gas_used: u64, + + pub gas_used: Option, + + /// Minimal reproduction test case for failing fuzz tests + pub counterexample: Option, } use std::marker::PhantomData; @@ -33,10 +50,14 @@ impl<'a, S, E> ContractRunner<'a, S, E> { impl<'a, S, E: Evm> ContractRunner<'a, S, E> where - E: Evm, + E: Evm + Clone, { /// Runs all tests for a contract whose names match the provided regular expression - pub fn run_tests(&mut self, regex: &Regex) -> Result> { + pub fn run_tests( + &mut self, + regex: &Regex, + fuzzer: Option<&mut TestRunner>, + ) -> Result> { let start = Instant::now(); let needs_setup = self.contract.abi.functions().any(|func| func.name == "setUp"); let test_fns = self @@ -45,22 +66,36 @@ where .functions() .into_iter() .filter(|func| func.name.starts_with("test")) - .filter(|func| regex.is_match(&func.name)); + .filter(|func| regex.is_match(&func.name)) + .collect::>(); - // run all tests - let map = test_fns + // run all unit tests + let unit_tests = test_fns + .iter() + .filter(|func| func.inputs.is_empty()) .map(|func| { - // call the setup function in each test to reset the test's state. - // if we did this outside the map, we'd not have test isolation - if needs_setup { - self.evm.setup(self.address)?; - } - - let result = self.run_test(func)?; + let result = self.run_test(func, needs_setup)?; Ok((func.name.clone(), result)) }) .collect::>>()?; + let map = if let Some(mut fuzzer) = fuzzer { + let fuzz_tests = test_fns + .iter() + .filter(|func| !func.inputs.is_empty()) + .map(|func| { + let result = self.run_fuzz_test(func, needs_setup, &mut fuzzer)?; + Ok((func.name.clone(), result)) + }) + .collect::>>()?; + + let mut map = unit_tests; + map.extend(fuzz_tests); + map + } else { + unit_tests + }; + if !map.is_empty() { let duration = Instant::now().duration_since(start); tracing::debug!("total duration: {:?}", duration); @@ -69,22 +104,78 @@ where } #[tracing::instrument(name = "test", skip_all, fields(name = %func.name))] - pub fn run_test(&mut self, func: &Function) -> Result { + pub fn run_test(&mut self, func: &Function, setup: bool) -> Result { let start = Instant::now(); - - // The test result data is not used anywhere. - let (_, reason, gas_used) = - self.evm.call::<(), _>(Address::zero(), self.address, func, (), 0.into())?; - let duration = Instant::now().duration_since(start); - // the expected result depends on the function name // DAppTools' ds-test will not revert inside its `assertEq`-like functions // which allows to test multiple assertions in 1 test function while also // preserving logs. - let success = self.evm.check_success(self.address, &reason, func.name.contains("testFail")); + let should_fail = func.name.contains("testFail"); + + // call the setup function in each test to reset the test's state. + if setup { + self.evm.setup(self.address)?; + } + + let (_, reason, gas_used) = + self.evm.call::<(), _>(Address::zero(), self.address, func, (), 0.into())?; + let success = self.evm.check_success(self.address, &reason, should_fail); + let duration = Instant::now().duration_since(start); tracing::trace!(?duration, %success, %gas_used); - Ok(TestResult { success, gas_used }) + Ok(TestResult { success, gas_used: Some(gas_used), counterexample: None }) + } + + #[tracing::instrument(name = "fuzz-test", skip_all, fields(name = %func.name))] + pub fn run_fuzz_test( + &mut self, + func: &Function, + setup: bool, + runner: &mut TestRunner, + ) -> Result { + // call the setup function in each test to reset the test's state. + if setup { + self.evm.setup(self.address)?; + } + + let start = Instant::now(); + let should_fail = func.name.contains("testFail"); + + // Get the calldata generation strategy for the function + let strat = crate::fuzz::fuzz_calldata(func); + + // Run the strategy + let result = runner.run(&strat, |calldata| { + let mut evm = self.evm.clone(); + + let (_, reason, _) = evm + .call_raw(Address::zero(), self.address, calldata, 0.into(), false) + .expect("could not make raw evm call"); + + let success = evm.check_success(self.address, &reason, should_fail); + + // This will panic and get caught by the executor + proptest::prop_assert!(success); + + Ok(()) + }); + + let (success, counterexample) = match result { + Ok(_) => (true, None), + Err(TestError::Fail(_, value)) => { + // skip the function selector when decoding + let args = func.decode_input(&value.as_ref()[4..])?; + let counterexample = CounterExample { calldata: value.clone(), args }; + tracing::info!("Found minimal failing case: {}", hex::encode(&value)); + (false, Some(counterexample)) + } + result => panic!("Unexpected result: {:?}", result), + }; + + let duration = Instant::now().duration_since(start); + tracing::trace!(?duration, %success); + + Ok(TestResult { success, gas_used: None, counterexample }) } } @@ -96,10 +187,12 @@ mod tests { use std::marker::PhantomData; mod sputnik { + use dapp_utils::get_func; use evm_adapters::sputnik::{ helpers::{new_backend, new_vicinity}, Executor, }; + use proptest::test_runner::Config as FuzzConfig; use super::*; @@ -113,6 +206,53 @@ mod tests { let evm = Executor::new(12_000_000, &cfg, &backend); super::test_runner(evm, addr, compiled); } + + #[test] + fn test_fuzz_shrinking() { + let cfg = Config::istanbul(); + let compiled = COMPILED.get("GreeterTest").expect("could not find contract"); + let addr = "0x1000000000000000000000000000000000000000".parse().unwrap(); + let vicinity = new_vicinity(); + let backend = new_backend(&vicinity, Default::default()); + + let mut evm = Executor::new(12_000_000, &cfg, &backend); + evm.initialize_contracts(vec![(addr, compiled.runtime_bytecode.clone())]); + + let mut runner = ContractRunner { + evm: &mut evm, + contract: compiled, + address: addr, + state: PhantomData, + }; + + let cfg = FuzzConfig::default(); + let mut fuzzer = TestRunner::new(cfg); + let func = get_func("function testFuzzShrinking(uint256 x, uint256 y) public").unwrap(); + let res = runner.run_fuzz_test(&func, true, &mut fuzzer).unwrap(); + assert!(!res.success); + + // get the counterexample with shrinking enabled by default + let counterexample = res.counterexample.unwrap(); + let product_with_shrinking: u64 = + // casting to u64 here is safe because the shrunk result is always gonna be small + // enough to fit in a u64, whereas as seen below, that's not possible without + // shrinking + counterexample.args.into_iter().map(|x| x.into_uint().unwrap().as_u64()).product(); + + let mut cfg = FuzzConfig::default(); + // we reduce the shrinking iters and observe a larger result + cfg.max_shrink_iters = 5; + let mut fuzzer = TestRunner::new(cfg); + let res = runner.run_fuzz_test(&func, true, &mut fuzzer).unwrap(); + assert!(!res.success); + + // get the non-shrunk result + let counterexample = res.counterexample.unwrap(); + let args = + counterexample.args.into_iter().map(|x| x.into_uint().unwrap()).collect::>(); + let product_without_shrinking = args[0].saturating_mul(args[1]); + assert!(product_without_shrinking > product_with_shrinking.into()); + } } mod evmodin { @@ -135,13 +275,17 @@ mod tests { } } - pub fn test_runner>(mut evm: E, addr: Address, compiled: &CompiledContract) { + pub fn test_runner>( + mut evm: E, + addr: Address, + compiled: &CompiledContract, + ) { evm.initialize_contracts(vec![(addr, compiled.runtime_bytecode.clone())]); let mut runner = ContractRunner { evm: &mut evm, contract: compiled, address: addr, state: PhantomData }; - let res = runner.run_tests(&".*".parse().unwrap()).unwrap(); + let res = runner.run_tests(&".*".parse().unwrap(), None).unwrap(); assert!(res.len() > 0); assert!(res.iter().all(|(_, result)| result.success == true)); } diff --git a/dapptools/Cargo.toml b/dapptools/Cargo.toml index 3cb05e2d5fe8..56001517df11 100644 --- a/dapptools/Cargo.toml +++ b/dapptools/Cargo.toml @@ -27,8 +27,9 @@ tracing = "0.1.26" ## EVM Implementations # evm = { version = "0.30.1" } -sputnik = { package = "evm", git = "https://github.com/gakonst/evm", optional = true } +sputnik = { package = "evm", git = "https://github.com/rust-blockchain/evm", optional = true } evmodin = { git = "https://github.com/vorot93/evmodin", optional = true } +proptest = "1.0.0" [features] default = ["sputnik-evm", "evmodin-evm"] diff --git a/dapptools/src/dapp.rs b/dapptools/src/dapp.rs index a22bb345148e..f23750157e81 100644 --- a/dapptools/src/dapp.rs +++ b/dapptools/src/dapp.rs @@ -42,6 +42,7 @@ fn main() -> eyre::Result<()> { .remappings(&remappings) .libraries(&lib_paths) .out_path(out_path) + .fuzzer(proptest::test_runner::TestRunner::default()) .skip_compilation(no_compile); // run the tests depending on the chosen EVM @@ -105,7 +106,7 @@ fn main() -> eyre::Result<()> { Ok(()) } -fn test>( +fn test>( builder: MultiContractRunnerBuilder, evm: E, pattern: Regex, @@ -134,7 +135,15 @@ fn test>( } else { Colour::Red.paint("[FAIL]") }; - println!("{} {} (gas: {})", status, name, result.gas_used); + println!( + "{} {} (gas: {})", + status, + name, + result + .gas_used + .map(|x| x.to_string()) + .unwrap_or_else(|| "[fuzztest]".to_string()) + ); } } } diff --git a/evm-adapters/Cargo.toml b/evm-adapters/Cargo.toml index f3b54eeffa0e..66fc5cbcf398 100644 --- a/evm-adapters/Cargo.toml +++ b/evm-adapters/Cargo.toml @@ -11,7 +11,7 @@ dapp-utils = { path = "./../utils" } dapp-solc = { path = "./../solc" } # evm = { version = "0.30.1" } -sputnik = { package = "evm", git = "https://github.com/gakonst/evm", optional = true } +sputnik = { package = "evm", git = "https://github.com/rust-blockchain/evm", optional = true } evmodin = { git = "https://github.com/vorot93/evmodin", optional = true } diff --git a/evm-adapters/src/blocking_provider.rs b/evm-adapters/src/blocking_provider.rs index e87280328c6f..b5e6b6eea4fc 100644 --- a/evm-adapters/src/blocking_provider.rs +++ b/evm-adapters/src/blocking_provider.rs @@ -12,6 +12,12 @@ pub struct BlockingProvider { runtime: Runtime, } +impl Clone for BlockingProvider { + fn clone(&self) -> Self { + Self { provider: self.provider.clone(), runtime: Runtime::new().unwrap() } + } +} + #[cfg(feature = "sputnik")] use sputnik::backend::MemoryVicinity; diff --git a/evm-adapters/src/evmodin.rs b/evm-adapters/src/evmodin.rs index d22552a3829e..f9655b4213e0 100644 --- a/evm-adapters/src/evmodin.rs +++ b/evm-adapters/src/evmodin.rs @@ -1,10 +1,6 @@ use crate::Evm; -use ethers::{ - abi::{Detokenize, Function, Tokenize}, - prelude::{decode_function_data, encode_function_data}, - types::{Address, Bytes, U256}, -}; +use ethers::types::{Address, Bytes, U256}; use evmodin::{tracing::Tracer, AnalyzedCode, CallKind, Host, Message, Revision, StatusCode}; @@ -12,6 +8,7 @@ use eyre::Result; // TODO: Check if we can implement this as the base layer of an ethers-provider // Middleware stack instead of doing RPC calls. +#[derive(Clone, Debug)] pub struct EvmOdin { pub host: S, pub gas_limit: u64, @@ -62,16 +59,14 @@ impl Evm for EvmOdin { } /// Runs the selected function - fn call( + fn call_raw( &mut self, from: Address, to: Address, - func: &Function, - args: T, // derive arbitrary for Tokenize? + calldata: Bytes, value: U256, - ) -> Result<(D, Self::ReturnReason, u64)> { - let calldata = encode_function_data(func, args)?; - + is_static: bool, + ) -> Result<(Bytes, Self::ReturnReason, u64)> { // For the `func.constant` field usage #[allow(deprecated)] let message = Message { @@ -83,11 +78,7 @@ impl Evm for EvmOdin { input_data: calldata.0, value, gas: self.gas_limit as i64, - is_static: func.constant || - matches!( - func.state_mutability, - ethers::abi::StateMutability::View | ethers::abi::StateMutability::Pure - ), + is_static, }; // get the bytecode at the host @@ -98,14 +89,11 @@ impl Evm for EvmOdin { let output = bytecode.execute(&mut self.host, &mut self.tracer, None, message, self.revision); - // let gas = dapp_utils::remove_extra_costs(gas_before - gas_after, calldata.as_ref()); - - let retdata = decode_function_data(func, output.output_data, false)?; - // TODO: Figure out gas accounting. + // let gas = dapp_utils::remove_extra_costs(gas_before - gas_after, calldata.as_ref()); let gas = U256::from(0); - Ok((retdata, output.status_code, gas.as_u64())) + Ok((output.output_data.to_vec().into(), output.status_code, gas.as_u64())) } } diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index 2d8cdd072f91..17a1f3ea10ce 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -12,7 +12,7 @@ pub use blocking_provider::BlockingProvider; use ethers::{ abi::{Detokenize, Function, Tokenize}, core::types::{Address, U256}, - prelude::Bytes, + prelude::{decode_function_data, encode_function_data, Bytes}, }; use dapp_utils::get_func; @@ -49,7 +49,27 @@ pub trait Evm { func: &Function, args: T, // derive arbitrary for Tokenize? value: U256, - ) -> Result<(D, Self::ReturnReason, u64)>; + ) -> Result<(D, Self::ReturnReason, u64)> { + let calldata = encode_function_data(func, args)?; + #[allow(deprecated)] + let is_static = func.constant || + matches!( + func.state_mutability, + ethers::abi::StateMutability::View | ethers::abi::StateMutability::Pure + ); + let (retdata, status, gas) = self.call_raw(from, to, calldata, value, is_static)?; + let retdata = decode_function_data(func, retdata, false)?; + Ok((retdata, status, gas)) + } + + fn call_raw( + &mut self, + from: Address, + to: Address, + calldata: Bytes, + value: U256, + is_static: bool, + ) -> Result<(Bytes, Self::ReturnReason, u64)>; /// Runs the `setUp()` function call to instantiate the contract's state fn setup(&mut self, address: Address) -> Result<()> { diff --git a/evm-adapters/src/sputnik/evm.rs b/evm-adapters/src/sputnik/evm.rs index d55676382200..d67c4df80a94 100644 --- a/evm-adapters/src/sputnik/evm.rs +++ b/evm-adapters/src/sputnik/evm.rs @@ -1,10 +1,6 @@ use crate::Evm; -use ethers::{ - abi::{Detokenize, Function, Tokenize}, - prelude::{decode_function_data, encode_function_data}, - types::{Address, Bytes, U256}, -}; +use ethers::types::{Address, Bytes, U256}; use sputnik::{ backend::{Backend, MemoryAccount}, @@ -24,6 +20,23 @@ pub struct Executor<'a, S> { pub gas_limit: u64, } +// Manual implementation of `Clone` for Clone-able StackStates (typically when the Backend +// behind them is also clone-able). This is useful to have e.g. when running fuzz +// tests which we need to take ownership of the EVM and clone it for each run in the +// test runner's closure. +impl<'a, S: StackState<'a> + Clone> Clone for Executor<'a, S> { + fn clone(&self) -> Self { + Self { + gas_limit: self.gas_limit, + executor: StackExecutor::new_with_precompile( + self.executor.state().clone(), + self.executor.config(), + Default::default(), + ), + } + } +} + // Concrete implementation over the in-memory backend impl<'a, B: Backend> Executor<'a, MemoryStackState<'a, 'a, B>> { /// Given a gas limit, vm version, initial chain configuration and initial state @@ -78,16 +91,14 @@ where } /// Runs the selected function - fn call( + fn call_raw( &mut self, from: Address, to: Address, - func: &Function, - args: T, // derive arbitrary for Tokenize? + calldata: Bytes, value: U256, - ) -> Result<(D, ExitReason, u64)> { - let calldata = encode_function_data(func, args)?; - + _is_static: bool, + ) -> Result<(Bytes, ExitReason, u64)> { let gas_before = self.executor.gas_left(); let (status, retdata) = @@ -96,9 +107,7 @@ where let gas_after = self.executor.gas_left(); let gas = dapp_utils::remove_extra_costs(gas_before - gas_after, calldata.as_ref()); - let retdata = decode_function_data(func, retdata, false)?; - - Ok((retdata, status, gas.as_u64())) + Ok((retdata.into(), status, gas.as_u64())) } } diff --git a/evm-adapters/src/sputnik/forked_backend.rs b/evm-adapters/src/sputnik/forked_backend.rs index 39067b0ca3af..83036deacf57 100644 --- a/evm-adapters/src/sputnik/forked_backend.rs +++ b/evm-adapters/src/sputnik/forked_backend.rs @@ -10,7 +10,7 @@ use std::collections::BTreeMap; /// Memory backend with ability to fork another chain from an HTTP provider, storing all state /// values in a `BTreeMap` in memory. -#[derive(Debug)] +#[derive(Clone, Debug)] // TODO: Add option to easily 1. impersonate accounts, 2. roll back to pinned block pub struct ForkMemoryBackend { /// ethers middleware for querying on-chain data