Skip to content

Commit 337a43f

Browse files
committed
REVM fuzzer (#789)
1 parent 030ebbf commit 337a43f

File tree

19 files changed

+854
-268
lines changed

19 files changed

+854
-268
lines changed

forge/src/executor/db/cache.rs

Lines changed: 0 additions & 144 deletions
This file was deleted.

forge/src/executor/db/mod.rs

Lines changed: 0 additions & 2 deletions
This file was deleted.

forge/src/executor/fuzz/mod.rs

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
mod strategies;
2+
3+
// TODO Port when we have cheatcodes again
4+
//use crate::{Evm, ASSUME_MAGIC_RETURN_CODE};
5+
use crate::executor::{Executor, RawCallResult};
6+
use ethers::{
7+
abi::{Abi, Function},
8+
types::{Address, Bytes},
9+
};
10+
use revm::{db::DatabaseRef, Return};
11+
use strategies::fuzz_calldata;
12+
13+
pub use proptest::test_runner::{Config as FuzzConfig, Reason};
14+
use proptest::test_runner::{TestError, TestRunner};
15+
use serde::{Deserialize, Serialize};
16+
use std::cell::RefCell;
17+
18+
/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/).
19+
///
20+
/// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with
21+
/// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the
22+
/// configuration which can be overridden via [environment variables](https://docs.rs/proptest/1.0.0/proptest/test_runner/struct.Config.html)
23+
pub struct FuzzedExecutor<'a, DB: DatabaseRef> {
24+
/// The VM
25+
executor: &'a Executor<DB>,
26+
/// The fuzzer
27+
runner: TestRunner,
28+
/// The account that calls tests
29+
sender: Address,
30+
}
31+
32+
impl<'a, DB> FuzzedExecutor<'a, DB>
33+
where
34+
DB: DatabaseRef,
35+
{
36+
/// Instantiates a fuzzed executor given a testrunner
37+
pub fn new(executor: &'a Executor<DB>, runner: TestRunner, sender: Address) -> Self {
38+
Self { executor, runner, sender }
39+
}
40+
41+
/// Fuzzes the provided function, assuming it is available at the contract at `address`
42+
/// If `should_fail` is set to `true`, then it will stop only when there's a success
43+
/// test case.
44+
///
45+
/// Returns a list of all the consumed gas and calldata of every fuzz case
46+
pub fn fuzz(
47+
&self,
48+
func: &Function,
49+
address: Address,
50+
should_fail: bool,
51+
abi: Option<&Abi>,
52+
) -> FuzzTestResult {
53+
let strat = fuzz_calldata(func);
54+
55+
// Stores the consumed gas and calldata of every successful fuzz call
56+
let fuzz_cases: RefCell<Vec<FuzzCase>> = RefCell::new(Default::default());
57+
58+
// Stores the latest return and revert reason of a test call
59+
let return_reason: RefCell<Option<Return>> = RefCell::new(None);
60+
let revert_reason = RefCell::new(None);
61+
62+
let mut runner = self.runner.clone();
63+
tracing::debug!(func = ?func.name, should_fail, "fuzzing");
64+
let test_error = runner
65+
.run(&strat, |calldata| {
66+
let RawCallResult { status, result, gas, state_changeset, .. } = self
67+
.executor
68+
.call_raw(self.sender, address, calldata.0.clone(), 0.into())
69+
.expect("could not make raw evm call");
70+
71+
// 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);
75+
let err = "ASSUME: Too many rejects";
76+
let _ = revert_reason.borrow_mut().insert(err.to_string());
77+
return Err(TestCaseError::Reject(err.into()));
78+
}*/
79+
80+
let success = self.executor.is_success(
81+
address,
82+
status,
83+
state_changeset.expect("we should have a state changeset"),
84+
should_fail,
85+
);
86+
87+
// Store the result of this test case
88+
let _ = return_reason.borrow_mut().insert(status);
89+
if !success {
90+
let revert =
91+
foundry_utils::decode_revert(result.as_ref(), abi).unwrap_or_default();
92+
let _ = revert_reason.borrow_mut().insert(revert);
93+
}
94+
95+
// This will panic and get caught by the executor
96+
proptest::prop_assert!(
97+
success,
98+
"{}, expected failure: {}, reason: '{}'",
99+
func.name,
100+
should_fail,
101+
match foundry_utils::decode_revert(result.as_ref(), abi) {
102+
Ok(e) => e,
103+
Err(e) => e.to_string(),
104+
}
105+
);
106+
107+
// Push test case to the case set
108+
fuzz_cases.borrow_mut().push(FuzzCase { calldata, gas });
109+
Ok(())
110+
})
111+
.err()
112+
.map(|test_error| FuzzError {
113+
test_error,
114+
return_reason: return_reason.into_inner().expect("Reason must be set"),
115+
revert_reason: revert_reason.into_inner().expect("Revert error string must be set"),
116+
});
117+
118+
FuzzTestResult { cases: FuzzedCases::new(fuzz_cases.into_inner()), test_error }
119+
}
120+
}
121+
122+
/// The outcome of a fuzz test
123+
pub struct FuzzTestResult {
124+
/// Every successful fuzz test case
125+
pub cases: FuzzedCases,
126+
/// if there was a case that resulted in an error, this contains the error and the return
127+
/// reason of the failed call
128+
pub test_error: Option<FuzzError>,
129+
}
130+
131+
impl FuzzTestResult {
132+
/// Returns `true` if all test cases succeeded
133+
pub fn is_ok(&self) -> bool {
134+
self.test_error.is_none()
135+
}
136+
137+
/// Returns `true` if a test case failed
138+
pub fn is_err(&self) -> bool {
139+
self.test_error.is_some()
140+
}
141+
}
142+
143+
pub struct FuzzError {
144+
/// The proptest error occurred as a result of a test case
145+
pub test_error: TestError<Bytes>,
146+
/// The return reason of the offending call
147+
pub return_reason: Return,
148+
/// The revert string of the offending call
149+
pub revert_reason: String,
150+
}
151+
152+
/// Container type for all successful test cases
153+
#[derive(Clone, Debug, Serialize, Deserialize)]
154+
#[serde(transparent)]
155+
pub struct FuzzedCases {
156+
cases: Vec<FuzzCase>,
157+
}
158+
159+
impl FuzzedCases {
160+
pub fn new(mut cases: Vec<FuzzCase>) -> Self {
161+
cases.sort_by_key(|c| c.gas);
162+
Self { cases }
163+
}
164+
165+
pub fn cases(&self) -> &[FuzzCase] {
166+
&self.cases
167+
}
168+
169+
pub fn into_cases(self) -> Vec<FuzzCase> {
170+
self.cases
171+
}
172+
173+
/// Returns the median gas of all test cases
174+
pub fn median_gas(&self) -> u64 {
175+
let mid = self.cases.len() / 2;
176+
self.cases.get(mid).map(|c| c.gas).unwrap_or_default()
177+
}
178+
179+
/// Returns the average gas use of all test cases
180+
pub fn mean_gas(&self) -> u64 {
181+
if self.cases.is_empty() {
182+
return 0
183+
}
184+
185+
(self.cases.iter().map(|c| c.gas as u128).sum::<u128>() / self.cases.len() as u128) as u64
186+
}
187+
188+
/// Returns the case with the highest gas usage
189+
pub fn highest(&self) -> Option<&FuzzCase> {
190+
self.cases.last()
191+
}
192+
193+
/// Returns the case with the lowest gas usage
194+
pub fn lowest(&self) -> Option<&FuzzCase> {
195+
self.cases.first()
196+
}
197+
198+
/// Returns the highest amount of gas spent on a fuzz case
199+
pub fn highest_gas(&self) -> u64 {
200+
self.highest().map(|c| c.gas).unwrap_or_default()
201+
}
202+
203+
/// Returns the lowest amount of gas spent on a fuzz case
204+
pub fn lowest_gas(&self) -> u64 {
205+
self.lowest().map(|c| c.gas).unwrap_or_default()
206+
}
207+
}
208+
209+
/// Data of a single fuzz test case
210+
#[derive(Clone, Debug, Serialize, Deserialize)]
211+
pub struct FuzzCase {
212+
/// The calldata used for this fuzz test
213+
pub calldata: Bytes,
214+
/// Consumed gas
215+
pub gas: u64,
216+
}
217+
218+
#[cfg(test)]
219+
mod tests {
220+
use super::*;
221+
222+
use crate::test_helpers::{fuzz_executor, test_executor, COMPILED};
223+
224+
#[test]
225+
fn prints_fuzzed_revert_reasons() {
226+
let mut executor = test_executor();
227+
228+
let compiled = COMPILED.find("FuzzTests").expect("could not find contract");
229+
let (addr, _, _, _) = executor
230+
.deploy(Address::zero(), compiled.bytecode().unwrap().0.clone(), 0.into())
231+
.unwrap();
232+
233+
let executor = fuzz_executor(&executor);
234+
235+
let func = compiled.abi.unwrap().function("testFuzzedRevert").unwrap();
236+
let res = executor.fuzz(func, addr, false, compiled.abi);
237+
let error = res.test_error.unwrap();
238+
let revert_reason = error.revert_reason;
239+
assert_eq!(revert_reason, "fuzztest-revert");
240+
}
241+
}

0 commit comments

Comments
 (0)