Skip to content

Commit b09359d

Browse files
committed
REVM cheatcodes (#841)
* 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
1 parent 337a43f commit b09359d

File tree

20 files changed

+1222
-126
lines changed

20 files changed

+1222
-126
lines changed

Cargo.lock

Lines changed: 3 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/cmd/test.rs

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use ethers::{
1212
contract::EthLogDecode,
1313
solc::{ArtifactOutput, ProjectCompileOutput},
1414
};
15-
use forge::{executor::opts::EvmOpts, MultiContractRunnerBuilder, TestFilter, TestResult};
15+
use forge::{
16+
decode::decode_console_logs, executor::opts::EvmOpts, MultiContractRunnerBuilder, TestFilter,
17+
TestResult,
18+
};
1619
use foundry_config::{figment::Figment, Config};
1720
use regex::Regex;
1821
use std::{collections::BTreeMap, str::FromStr, sync::mpsc::channel, thread};
@@ -367,9 +370,7 @@ fn test<A: ArtifactOutput + 'static>(
367370
let mut add_newline = false;
368371
if verbosity > 1 && !result.logs.is_empty() {
369372
// We only decode logs from Hardhat and DS-style console events
370-
let console_logs: Vec<String> =
371-
result.logs.iter().filter_map(decode_console_log).collect();
372-
373+
let console_logs = decode_console_logs(&result.logs);
373374
if !console_logs.is_empty() {
374375
println!("Logs:");
375376
for log in console_logs {
@@ -462,41 +463,3 @@ fn test<A: ArtifactOutput + 'static>(
462463
Ok(TestOutcome::new(results, allow_failure))
463464
}
464465
}
465-
466-
fn decode_console_log(log: &RawLog) -> Option<String> {
467-
use forge::abi::ConsoleEvents::{self, *};
468-
469-
let decoded = match ConsoleEvents::decode_log(log).ok()? {
470-
LogsFilter(inner) => format!("{}", inner.0),
471-
LogBytesFilter(inner) => format!("{}", inner.0),
472-
LogNamedAddressFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
473-
LogNamedBytes32Filter(inner) => {
474-
format!("{}: 0x{}", inner.key, hex::encode(inner.val))
475-
}
476-
LogNamedDecimalIntFilter(inner) => {
477-
let (sign, val) = inner.val.into_sign_and_abs();
478-
format!(
479-
"{}: {}{}",
480-
inner.key,
481-
sign,
482-
ethers::utils::format_units(val, inner.decimals.as_u32()).unwrap()
483-
)
484-
}
485-
LogNamedDecimalUintFilter(inner) => {
486-
format!(
487-
"{}: {}",
488-
inner.key,
489-
ethers::utils::format_units(inner.val, inner.decimals.as_u32()).unwrap()
490-
)
491-
}
492-
LogNamedIntFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
493-
LogNamedUintFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
494-
LogNamedBytesFilter(inner) => {
495-
format!("{}: 0x{}", inner.key, hex::encode(inner.val))
496-
}
497-
LogNamedStringFilter(inner) => format!("{}: {}", inner.key, inner.val),
498-
499-
e => e.to_string(),
500-
};
501-
Some(decoded)
502-
}

forge/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ rlp = "0.5.1"
2525

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

forge/src/decode.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//! Various utilities to decode test results
2+
use crate::abi::ConsoleEvents::{self, *};
3+
use ethers::{abi::RawLog, contract::EthLogDecode};
4+
5+
/// Decode a set of logs, only returning logs from DSTest logging events and Hardhat's `console.log`
6+
pub fn decode_console_logs(logs: &[RawLog]) -> Vec<String> {
7+
logs.iter().filter_map(decode_console_log).collect()
8+
}
9+
10+
/// Decode a single log.
11+
///
12+
/// This function returns [None] if it is not a DSTest log or the result of a Hardhat
13+
/// `console.log`.
14+
pub fn decode_console_log(log: &RawLog) -> Option<String> {
15+
let decoded = match ConsoleEvents::decode_log(log).ok()? {
16+
LogsFilter(inner) => format!("{}", inner.0),
17+
LogBytesFilter(inner) => format!("{}", inner.0),
18+
LogNamedAddressFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
19+
LogNamedBytes32Filter(inner) => {
20+
format!("{}: 0x{}", inner.key, hex::encode(inner.val))
21+
}
22+
LogNamedDecimalIntFilter(inner) => {
23+
let (sign, val) = inner.val.into_sign_and_abs();
24+
format!(
25+
"{}: {}{}",
26+
inner.key,
27+
sign,
28+
ethers::utils::format_units(val, inner.decimals.as_u32()).unwrap()
29+
)
30+
}
31+
LogNamedDecimalUintFilter(inner) => {
32+
format!(
33+
"{}: {}",
34+
inner.key,
35+
ethers::utils::format_units(inner.val, inner.decimals.as_u32()).unwrap()
36+
)
37+
}
38+
LogNamedIntFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
39+
LogNamedUintFilter(inner) => format!("{}: {:?}", inner.key, inner.val),
40+
LogNamedBytesFilter(inner) => {
41+
format!("{}: 0x{}", inner.key, hex::encode(inner.val))
42+
}
43+
LogNamedStringFilter(inner) => format!("{}: {}", inner.key, inner.val),
44+
45+
e => e.to_string(),
46+
};
47+
Some(decoded)
48+
}

forge/src/executor/abi.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,47 @@ use ethers::types::{Address, Selector};
22
use once_cell::sync::Lazy;
33
use std::collections::HashMap;
44

5+
/// The cheatcode handler address.
6+
///
7+
/// This is the same address as the one used in DappTools's HEVM.
8+
pub static CHEATCODE_ADDRESS: Lazy<Address> = Lazy::new(|| {
9+
Address::from_slice(&hex::decode("7109709ECfa91a80626fF3989D68f67F5b1DD12D").unwrap())
10+
});
11+
12+
// Bindings for cheatcodes
13+
ethers::contract::abigen!(
14+
HEVM,
15+
r#"[
16+
roll(uint256)
17+
warp(uint256)
18+
fee(uint256)
19+
store(address,bytes32,bytes32)
20+
load(address,bytes32)(bytes32)
21+
ffi(string[])(bytes)
22+
addr(uint256)(address)
23+
sign(uint256,bytes32)(uint8,bytes32,bytes32)
24+
prank(address)
25+
startPrank(address)
26+
prank(address,address)
27+
startPrank(address,address)
28+
stopPrank()
29+
deal(address,uint256)
30+
etch(address,bytes)
31+
expectRevert(bytes)
32+
expectRevert(bytes4)
33+
record()
34+
accesses(address)(bytes32[],bytes32[])
35+
expectEmit(bool,bool,bool,bool)
36+
mockCall(address,bytes,bytes)
37+
clearMockedCalls()
38+
expectCall(address,bytes)
39+
getCode(string)
40+
label(address,string)
41+
assume(bool)
42+
]"#,
43+
);
44+
pub use hevm_mod::{HEVMCalls, HEVM_ABI};
45+
546
/// The Hardhat console address.
647
///
748
/// See: https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-core/console.sol

forge/src/executor/builder.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,44 @@
11
use revm::{db::EmptyDB, Env, SpecId};
22

3-
use super::Executor;
3+
use super::{inspector::InspectorStackConfig, Executor};
44

55
#[derive(Default)]
66
pub struct ExecutorBuilder {
7-
/// Whether or not cheatcodes are enabled
8-
cheatcodes: bool,
9-
/// Whether or not the FFI cheatcode is enabled
10-
ffi: bool,
117
/// The execution environment configuration.
12-
config: Env,
8+
env: Env,
9+
/// The configuration used to build an [InspectorStack].
10+
inspector_config: InspectorStackConfig,
1311
}
1412

1513
impl ExecutorBuilder {
1614
#[must_use]
1715
pub fn new() -> Self {
18-
Self { cheatcodes: false, ffi: false, config: Env::default() }
16+
Default::default()
1917
}
2018

2119
/// Enables cheatcodes on the executor.
2220
#[must_use]
2321
pub fn with_cheatcodes(mut self, ffi: bool) -> Self {
24-
self.cheatcodes = true;
25-
self.ffi = ffi;
22+
self.inspector_config.cheatcodes = true;
23+
self.inspector_config.ffi = ffi;
2624
self
2725
}
2826

2927
pub fn with_spec(mut self, spec: SpecId) -> Self {
30-
self.config.cfg.spec_id = spec;
28+
self.env.cfg.spec_id = spec;
3129
self
3230
}
3331

3432
/// Configure the execution environment (gas limit, chain spec, ...)
3533
#[must_use]
36-
pub fn with_config(mut self, config: Env) -> Self {
37-
self.config = config;
34+
pub fn with_config(mut self, env: Env) -> Self {
35+
self.env = env;
3836
self
3937
}
4038

4139
/// Builds the executor as configured.
4240
pub fn build(self) -> Executor<EmptyDB> {
43-
Executor::new(EmptyDB(), self.config)
41+
Executor::new(EmptyDB(), self.env, self.inspector_config)
4442
}
4543

4644
// TODO: add with_traces

forge/src/executor/fuzz/mod.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
mod strategies;
22

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

1311
pub use proptest::test_runner::{Config as FuzzConfig, Reason};
14-
use proptest::test_runner::{TestError, TestRunner};
12+
use proptest::test_runner::{TestCaseError, TestError, TestRunner};
1513
use serde::{Deserialize, Serialize};
1614
use std::cell::RefCell;
1715

16+
/// Magic return code for the `assume` cheatcode
17+
pub const ASSUME_MAGIC_RETURN_CODE: &[u8] = "FOUNDRY::ASSUME".as_bytes();
18+
1819
/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/).
1920
///
2021
/// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with
@@ -69,13 +70,12 @@ where
6970
.expect("could not make raw evm call");
7071

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

8080
let success = self.executor.is_success(
8181
address,

0 commit comments

Comments
 (0)