Skip to content

Commit

Permalink
REVM cheatcodes (#841)
Browse files Browse the repository at this point in the history
* feat: add `InspectorStack`

Adds `InspectorStack`, an inspector that calls a stack
of other inspectors sequentially.

Closes #752

* feat: port cheatcodes to revm

* feat: port `expectCall` cheatcode

* feat: extract labels from cheatcode inspector

* feat: port `expectEmit` cheatcode

* refactor: move log decoding into `forge` crate

* chore: remove unused evm patch

* test: re-enable debug logs test

* fix: record reads on `SSTORE` ops

* refactor: rename `record` to `start_record`

* docs: clarify why `DUMMY_CALL_OUTPUT` is 320 bytes

* fix: handle `expectRevert` with no return data

* build: bump revm

* chore: remove outdated todo

* refactor: use static dispatch in `InspectorStack`

* build: use k256

* fix: make gas usage not so crazy
  • Loading branch information
onbjerg committed Mar 12, 2022
1 parent 6ff9c18 commit b962d97
Show file tree
Hide file tree
Showing 20 changed files with 1,222 additions and 126 deletions.
24 changes: 3 additions & 21 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 5 additions & 42 deletions cli/src/cmd/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use ethers::{
contract::EthLogDecode,
solc::{ArtifactOutput, ProjectCompileOutput},
};
use forge::{executor::opts::EvmOpts, MultiContractRunnerBuilder, TestFilter, TestResult};
use forge::{
decode::decode_console_logs, executor::opts::EvmOpts, MultiContractRunnerBuilder, TestFilter,
TestResult,
};
use foundry_config::{figment::Figment, Config};
use regex::Regex;
use std::{collections::BTreeMap, str::FromStr, sync::mpsc::channel, thread};
Expand Down Expand Up @@ -356,9 +359,7 @@ fn test<A: ArtifactOutput + 'static>(
let mut add_newline = false;
if verbosity > 1 && !result.logs.is_empty() {
// We only decode logs from Hardhat and DS-style console events
let console_logs: Vec<String> =
result.logs.iter().filter_map(decode_console_log).collect();

let console_logs = decode_console_logs(&result.logs);
if !console_logs.is_empty() {
println!("Logs:");
for log in console_logs {
Expand Down Expand Up @@ -451,41 +452,3 @@ fn test<A: ArtifactOutput + 'static>(
Ok(TestOutcome::new(results, allow_failure))
}
}

fn decode_console_log(log: &RawLog) -> Option<String> {
use forge::abi::ConsoleEvents::{self, *};

let decoded = match ConsoleEvents::decode_log(log).ok()? {
LogsFilter(inner) => format!("{}", inner.0),
LogBytesFilter(inner) => format!("{}", inner.0),
LogNamedAddressFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
LogNamedBytes32Filter(inner) => {
format!("{}: 0x{}", inner.key, hex::encode(inner.val))
}
LogNamedDecimalIntFilter(inner) => {
let (sign, val) = inner.val.into_sign_and_abs();
format!(
"{}: {}{}",
inner.key,
sign,
ethers::utils::format_units(val, inner.decimals.as_u32()).unwrap()
)
}
LogNamedDecimalUintFilter(inner) => {
format!(
"{}: {}",
inner.key,
ethers::utils::format_units(inner.val, inner.decimals.as_u32()).unwrap()
)
}
LogNamedIntFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
LogNamedUintFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
LogNamedBytesFilter(inner) => {
format!("{}: 0x{}", inner.key, hex::encode(inner.val))
}
LogNamedStringFilter(inner) => format!("{}: {}", inner.key, inner.val),

e => e.to_string(),
};
Some(decoded)
}
2 changes: 1 addition & 1 deletion forge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ rlp = "0.5.1"

bytes = "1.1.0"
thiserror = "1.0.29"
revm = { package = "revm", git = "https://github.com/bluealloy/revm", branch = "main" }
revm = { package = "revm", git = "https://github.com/onbjerg/revm", branch = "onbjerg/blockhashes", default-features = false, features = ["std", "k256"] }
hashbrown = "0.12"
once_cell = "1.9.0"

Expand Down
48 changes: 48 additions & 0 deletions forge/src/decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! Various utilities to decode test results
use crate::abi::ConsoleEvents::{self, *};
use ethers::{abi::RawLog, contract::EthLogDecode};

/// Decode a set of logs, only returning logs from DSTest logging events and Hardhat's `console.log`
pub fn decode_console_logs(logs: &[RawLog]) -> Vec<String> {
logs.iter().filter_map(decode_console_log).collect()
}

/// Decode a single log.
///
/// This function returns [None] if it is not a DSTest log or the result of a Hardhat
/// `console.log`.
pub fn decode_console_log(log: &RawLog) -> Option<String> {
let decoded = match ConsoleEvents::decode_log(log).ok()? {
LogsFilter(inner) => format!("{}", inner.0),
LogBytesFilter(inner) => format!("{}", inner.0),
LogNamedAddressFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
LogNamedBytes32Filter(inner) => {
format!("{}: 0x{}", inner.key, hex::encode(inner.val))
}
LogNamedDecimalIntFilter(inner) => {
let (sign, val) = inner.val.into_sign_and_abs();
format!(
"{}: {}{}",
inner.key,
sign,
ethers::utils::format_units(val, inner.decimals.as_u32()).unwrap()
)
}
LogNamedDecimalUintFilter(inner) => {
format!(
"{}: {}",
inner.key,
ethers::utils::format_units(inner.val, inner.decimals.as_u32()).unwrap()
)
}
LogNamedIntFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
LogNamedUintFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
LogNamedBytesFilter(inner) => {
format!("{}: 0x{}", inner.key, hex::encode(inner.val))
}
LogNamedStringFilter(inner) => format!("{}: {}", inner.key, inner.val),

e => e.to_string(),
};
Some(decoded)
}
41 changes: 41 additions & 0 deletions forge/src/executor/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@ use ethers::types::{Address, Selector};
use once_cell::sync::Lazy;
use std::collections::HashMap;

/// The cheatcode handler address.
///
/// This is the same address as the one used in DappTools's HEVM.
pub static CHEATCODE_ADDRESS: Lazy<Address> = Lazy::new(|| {
Address::from_slice(&hex::decode("7109709ECfa91a80626fF3989D68f67F5b1DD12D").unwrap())
});

// Bindings for cheatcodes
ethers::contract::abigen!(
HEVM,
r#"[
roll(uint256)
warp(uint256)
fee(uint256)
store(address,bytes32,bytes32)
load(address,bytes32)(bytes32)
ffi(string[])(bytes)
addr(uint256)(address)
sign(uint256,bytes32)(uint8,bytes32,bytes32)
prank(address)
startPrank(address)
prank(address,address)
startPrank(address,address)
stopPrank()
deal(address,uint256)
etch(address,bytes)
expectRevert(bytes)
expectRevert(bytes4)
record()
accesses(address)(bytes32[],bytes32[])
expectEmit(bool,bool,bool,bool)
mockCall(address,bytes,bytes)
clearMockedCalls()
expectCall(address,bytes)
getCode(string)
label(address,string)
assume(bool)
]"#,
);
pub use hevm_mod::{HEVMCalls, HEVM_ABI};

/// The Hardhat console address.
///
/// See: https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-core/console.sol
Expand Down
24 changes: 11 additions & 13 deletions forge/src/executor/builder.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,44 @@
use revm::{db::EmptyDB, Env, SpecId};

use super::Executor;
use super::{inspector::InspectorStackConfig, Executor};

#[derive(Default)]
pub struct ExecutorBuilder {
/// Whether or not cheatcodes are enabled
cheatcodes: bool,
/// Whether or not the FFI cheatcode is enabled
ffi: bool,
/// The execution environment configuration.
config: Env,
env: Env,
/// The configuration used to build an [InspectorStack].
inspector_config: InspectorStackConfig,
}

impl ExecutorBuilder {
#[must_use]
pub fn new() -> Self {
Self { cheatcodes: false, ffi: false, config: Env::default() }
Default::default()
}

/// Enables cheatcodes on the executor.
#[must_use]
pub fn with_cheatcodes(mut self, ffi: bool) -> Self {
self.cheatcodes = true;
self.ffi = ffi;
self.inspector_config.cheatcodes = true;
self.inspector_config.ffi = ffi;
self
}

pub fn with_spec(mut self, spec: SpecId) -> Self {
self.config.cfg.spec_id = spec;
self.env.cfg.spec_id = spec;
self
}

/// Configure the execution environment (gas limit, chain spec, ...)
#[must_use]
pub fn with_config(mut self, config: Env) -> Self {
self.config = config;
pub fn with_config(mut self, env: Env) -> Self {
self.env = env;
self
}

/// Builds the executor as configured.
pub fn build(self) -> Executor<EmptyDB> {
Executor::new(EmptyDB(), self.config)
Executor::new(EmptyDB(), self.env, self.inspector_config)
}

// TODO: add with_traces
Expand Down
18 changes: 9 additions & 9 deletions forge/src/executor/fuzz/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
mod strategies;

// TODO Port when we have cheatcodes again
//use crate::{Evm, ASSUME_MAGIC_RETURN_CODE};
use crate::executor::{Executor, RawCallResult};
use ethers::{
abi::{Abi, Function},
Expand All @@ -11,10 +9,13 @@ use revm::{db::DatabaseRef, Return};
use strategies::fuzz_calldata;

pub use proptest::test_runner::{Config as FuzzConfig, Reason};
use proptest::test_runner::{TestError, TestRunner};
use proptest::test_runner::{TestCaseError, TestError, TestRunner};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;

/// Magic return code for the `assume` cheatcode
pub const ASSUME_MAGIC_RETURN_CODE: &[u8] = "FOUNDRY::ASSUME".as_bytes();

/// Wrapper around an [`Executor`] 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
Expand Down Expand Up @@ -69,13 +70,12 @@ where
.expect("could not make raw evm call");

// When assume cheat code is triggered return a special string "FOUNDRY::ASSUME"
// TODO: Re-implement when cheatcodes are ported
/*if returndata.as_ref() == ASSUME_MAGIC_RETURN_CODE {
let _ = return_reason.borrow_mut().insert(reason);
if result.as_ref() == ASSUME_MAGIC_RETURN_CODE {
*return_reason.borrow_mut() = Some(status);
let err = "ASSUME: Too many rejects";
let _ = revert_reason.borrow_mut().insert(err.to_string());
return Err(TestCaseError::Reject(err.into()));
}*/
*revert_reason.borrow_mut() = Some(err.to_string());
return Err(TestCaseError::Reject(err.into()))
}

let success = self.executor.is_success(
address,
Expand Down
Loading

0 comments on commit b962d97

Please sign in to comment.