Skip to content

Commit fdfaafd

Browse files
authored
feat(cheatcodes): display cheatcode name in error message (#8533)
* feat(cheatcodes): display cheatcode name in error message * fix: tests * fix * rm * fix * fix
1 parent 5161091 commit fdfaafd

File tree

17 files changed

+100
-85
lines changed

17 files changed

+100
-85
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/Cargo.toml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,24 @@ parking_lot.workspace = true
4545
alloy-consensus = { workspace = true, features = ["k256"] }
4646
alloy-rlp.workspace = true
4747

48+
base64.workspace = true
49+
chrono.workspace = true
50+
dialoguer = "0.11.0"
4851
eyre.workspace = true
4952
itertools.workspace = true
5053
jsonpath_lib.workspace = true
54+
k256.workspace = true
55+
memchr = "2.7"
56+
p256 = "0.13.2"
57+
rand = "0.8"
5158
revm.workspace = true
59+
rustc-hash.workspace = true
60+
semver.workspace = true
5261
serde_json.workspace = true
53-
base64.workspace = true
62+
thiserror.workspace = true
5463
toml = { workspace = true, features = ["preserve_order"] }
5564
tracing.workspace = true
56-
k256.workspace = true
5765
walkdir.workspace = true
58-
p256 = "0.13.2"
59-
thiserror.workspace = true
60-
semver.workspace = true
61-
rustc-hash.workspace = true
62-
dialoguer = "0.11.0"
63-
rand = "0.8"
64-
chrono.workspace = true
6566

6667
[dev-dependencies]
6768
proptest.workspace = true

crates/cheatcodes/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ the Foundry cheatcodes externally.
2727

2828
For example, here are some tools that make use of the JSON interface:
2929
- Internally, this is used to generate [a simple Solidity interface](../../testdata/cheats/Vm.sol) for testing
30-
- (WIP) Used by [`forge-std`](https://github.com/foundry-rs/forge-std) to generate [user-friendly Solidity interfaces](https://github.com/foundry-rs/forge-std/blob/master/src/Vm.sol)
30+
- Used by [`forge-std`](https://github.com/foundry-rs/forge-std) to generate [user-friendly Solidity interfaces](https://github.com/foundry-rs/forge-std/blob/master/src/Vm.sol)
3131
- (WIP) Used by [the Foundry book](https://github.com/foundry-rs/book) to generate [the cheatcodes reference](https://book.getfoundry.sh/cheatcodes)
3232
- ...
3333

crates/cheatcodes/src/error.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,14 @@ macro_rules! ensure {
6868
macro_rules! ensure_not_precompile {
6969
($address:expr, $ctxt:expr) => {
7070
if $ctxt.is_precompile($address) {
71-
return Err($crate::error::precompile_error(
72-
<Self as $crate::CheatcodeDef>::CHEATCODE.func.id,
73-
$address,
74-
))
71+
return Err($crate::error::precompile_error($address));
7572
}
7673
};
7774
}
7875

7976
#[cold]
80-
pub(crate) fn precompile_error(id: &'static str, address: &Address) -> Error {
81-
fmt_err!("cannot call `{id}` on precompile {address}")
77+
pub(crate) fn precompile_error(address: &Address) -> Error {
78+
fmt_err!("cannot use precompile {address} as an argument")
8279
}
8380

8481
/// Error thrown by cheatcodes.
@@ -158,7 +155,7 @@ impl Error {
158155
}
159156

160157
/// Returns the kind of this error.
161-
#[inline(always)]
158+
#[inline]
162159
pub fn kind(&self) -> ErrorKind<'_> {
163160
let data = self.data();
164161
if self.is_str {
@@ -170,32 +167,38 @@ impl Error {
170167
}
171168

172169
/// Returns the raw data of this error.
173-
#[inline(always)]
170+
#[inline]
174171
pub fn data(&self) -> &[u8] {
175172
unsafe { &*self.data }
176173
}
177174

178-
#[inline(always)]
175+
/// Returns `true` if this error is a human-readable string.
176+
#[inline]
177+
pub fn is_str(&self) -> bool {
178+
self.is_str
179+
}
180+
181+
#[inline]
179182
fn new_str(data: &'static str) -> Self {
180183
Self::_new(true, false, data.as_bytes())
181184
}
182185

183-
#[inline(always)]
186+
#[inline]
184187
fn new_string(data: String) -> Self {
185188
Self::_new(true, true, Box::into_raw(data.into_boxed_str().into_boxed_bytes()))
186189
}
187190

188-
#[inline(always)]
191+
#[inline]
189192
fn new_bytes(data: &'static [u8]) -> Self {
190193
Self::_new(false, false, data)
191194
}
192195

193-
#[inline(always)]
196+
#[inline]
194197
fn new_vec(data: Vec<u8>) -> Self {
195198
Self::_new(false, true, Box::into_raw(data.into_boxed_slice()))
196199
}
197200

198-
#[inline(always)]
201+
#[inline]
199202
fn _new(is_str: bool, drop: bool, data: *const [u8]) -> Self {
200203
debug_assert!(!data.is_null());
201204
Self { is_str, drop, data }

crates/cheatcodes/src/evm.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,10 @@ impl Cheatcode for resumeGasMeteringCall {
275275
impl Cheatcode for lastCallGasCall {
276276
fn apply(&self, state: &mut Cheatcodes) -> Result {
277277
let Self {} = self;
278-
ensure!(state.last_call_gas.is_some(), "`lastCallGas` is only available after a call");
279-
Ok(state
280-
.last_call_gas
281-
.as_ref()
282-
// This should never happen, as we ensure `last_call_gas` is `Some` above.
283-
.expect("`lastCallGas` is only available after a call")
284-
.abi_encode())
278+
let Some(last_call_gas) = &state.last_call_gas else {
279+
bail!("no external call was made yet");
280+
};
281+
Ok(last_call_gas.abi_encode())
285282
}
286283
}
287284

@@ -354,7 +351,7 @@ impl Cheatcode for blobhashesCall {
354351
let Self { hashes } = self;
355352
ensure!(
356353
ccx.ecx.spec_id() >= SpecId::CANCUN,
357-
"`blobhash` is not supported before the Cancun hard fork; \
354+
"`blobhashes` is not supported before the Cancun hard fork; \
358355
see EIP-4844: https://eips.ethereum.org/EIPS/eip-4844"
359356
);
360357
ccx.ecx.env.tx.blob_hashes.clone_from(hashes);
@@ -367,7 +364,7 @@ impl Cheatcode for getBlobhashesCall {
367364
let Self {} = self;
368365
ensure!(
369366
ccx.ecx.spec_id() >= SpecId::CANCUN,
370-
"`blobhash` is not supported before the Cancun hard fork; \
367+
"`getBlobhashes` is not supported before the Cancun hard fork; \
371368
see EIP-4844: https://eips.ethereum.org/EIPS/eip-4844"
372369
);
373370
Ok(ccx.ecx.env.tx.blob_hashes.clone().abi_encode())
@@ -605,9 +602,8 @@ impl Cheatcode for broadcastRawTransactionCall {
605602
executor: &mut E,
606603
) -> Result {
607604
let mut data = self.data.as_ref();
608-
let tx = TxEnvelope::decode(&mut data).map_err(|err| {
609-
fmt_err!("broadcastRawTransaction: error decoding transaction ({err})")
610-
})?;
605+
let tx = TxEnvelope::decode(&mut data)
606+
.map_err(|err| fmt_err!("failed to decode RLP-encoded transaction: {err}"))?;
611607

612608
ccx.ecx.db.transact_from_tx(
613609
tx.clone().into(),

crates/cheatcodes/src/fs.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<B
352352
}
353353

354354
let version = if let Some(version) = version {
355-
Some(Version::parse(version).map_err(|_| fmt_err!("Error parsing version"))?)
355+
Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?)
356356
} else {
357357
None
358358
};
@@ -388,7 +388,7 @@ fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<B
388388
.collect::<Vec<_>>();
389389

390390
let artifact = match &filtered[..] {
391-
[] => Err(fmt_err!("No matching artifact found")),
391+
[] => Err(fmt_err!("no matching artifact found")),
392392
[artifact] => Ok(artifact),
393393
filtered => {
394394
// If we know the current script/test contract solc version, try to filter by it
@@ -403,7 +403,7 @@ fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<B
403403
.collect::<Vec<_>>();
404404
(filtered.len() == 1).then(|| filtered[0])
405405
})
406-
.ok_or_else(|| fmt_err!("Multiple matching artifacts found"))
406+
.ok_or_else(|| fmt_err!("multiple matching artifacts found"))
407407
}
408408
}?;
409409

@@ -414,7 +414,7 @@ fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<B
414414
};
415415

416416
return maybe_bytecode
417-
.ok_or_else(|| fmt_err!("No bytecode for contract. Is it abstract or unlinked?"));
417+
.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"));
418418
} else {
419419
let path_in_artifacts =
420420
match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
@@ -439,7 +439,7 @@ fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<B
439439
let data = fs::read_to_string(path)?;
440440
let artifact = serde_json::from_str::<ContractObject>(&data)?;
441441
let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode };
442-
maybe_bytecode.ok_or_else(|| fmt_err!("No bytecode for contract. Is it abstract or unlinked?"))
442+
maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
443443
}
444444

445445
impl Cheatcode for ffiCall {

crates/cheatcodes/src/inspector.rs

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ impl Cheatcodes {
592592
{
593593
let expected_revert = std::mem::take(&mut self.expected_revert).unwrap();
594594
return match expect::handle_expect_revert(
595+
false,
595596
true,
596597
expected_revert.reason.as_deref(),
597598
outcome.result.result,
@@ -1035,7 +1036,7 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
10351036
// Handle expected reverts
10361037
if let Some(expected_revert) = &self.expected_revert {
10371038
if ecx.journaled_state.depth() <= expected_revert.depth {
1038-
let needs_processing: bool = match expected_revert.kind {
1039+
let needs_processing = match expected_revert.kind {
10391040
ExpectedRevertKind::Default => !cheatcode_call,
10401041
// `pending_processing` == true means that we're in the `call_end` hook for
10411042
// `vm.expectCheatcodeRevert` and shouldn't expect revert here
@@ -1047,6 +1048,7 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
10471048
if needs_processing {
10481049
let expected_revert = std::mem::take(&mut self.expected_revert).unwrap();
10491050
return match expect::handle_expect_revert(
1051+
cheatcode_call,
10501052
false,
10511053
expected_revert.reason.as_deref(),
10521054
outcome.result.result,
@@ -1071,9 +1073,7 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
10711073
if let ExpectedRevertKind::Cheatcode { pending_processing } =
10721074
&mut self.expected_revert.as_mut().unwrap().kind
10731075
{
1074-
if *pending_processing {
1075-
*pending_processing = false;
1076-
}
1076+
*pending_processing = false;
10771077
}
10781078
}
10791079
}
@@ -1847,22 +1847,49 @@ fn apply_dispatch<DB: DatabaseExt, E: CheatcodesExecutor>(
18471847
};
18481848
}
18491849

1850-
let _guard = trace_span_and_call(calls);
1851-
let result = vm_calls!(dispatch);
1852-
trace_return(&result);
1850+
let mut dyn_cheat = DynCheatCache::new(calls);
1851+
let _guard = trace_span_and_call(&mut dyn_cheat);
1852+
let mut result = vm_calls!(dispatch);
1853+
fill_and_trace_return(&mut dyn_cheat, &mut result);
18531854
result
18541855
}
18551856

1856-
fn trace_span_and_call(calls: &Vm::VmCalls) -> tracing::span::EnteredSpan {
1857-
let mut cheat = None;
1858-
let mut get_cheat = || *cheat.get_or_insert_with(|| calls_as_dyn_cheatcode(calls));
1859-
let span = debug_span!(target: "cheatcodes", "apply", id = %get_cheat().id());
1857+
// Caches the result of `calls_as_dyn_cheatcode`.
1858+
// TODO: Remove this once Cheatcode is object-safe, as caching would not be necessary anymore.
1859+
struct DynCheatCache<'a> {
1860+
calls: &'a Vm::VmCalls,
1861+
slot: Option<&'a dyn DynCheatcode>,
1862+
}
1863+
1864+
impl<'a> DynCheatCache<'a> {
1865+
fn new(calls: &'a Vm::VmCalls) -> Self {
1866+
Self { calls, slot: None }
1867+
}
1868+
1869+
fn get(&mut self) -> &dyn DynCheatcode {
1870+
*self.slot.get_or_insert_with(|| calls_as_dyn_cheatcode(self.calls))
1871+
}
1872+
}
1873+
1874+
fn trace_span_and_call(dyn_cheat: &mut DynCheatCache) -> tracing::span::EnteredSpan {
1875+
let span = debug_span!(target: "cheatcodes", "apply", id = %dyn_cheat.get().id());
18601876
let entered = span.entered();
1861-
trace!(target: "cheatcodes", cheat = ?get_cheat().as_debug(), "applying");
1877+
trace!(target: "cheatcodes", cheat = ?dyn_cheat.get().as_debug(), "applying");
18621878
entered
18631879
}
18641880

1865-
fn trace_return(result: &Result) {
1881+
fn fill_and_trace_return(dyn_cheat: &mut DynCheatCache, result: &mut Result) {
1882+
if let Err(e) = result {
1883+
if e.is_str() {
1884+
let name = dyn_cheat.get().name();
1885+
// Skip showing the cheatcode name for:
1886+
// - assertions: too verbose, and can already be inferred from the error message
1887+
// - `rpcUrl`: forge-std relies on it in `getChainWithUpdatedRpcUrl`
1888+
if !name.contains("assert") && name != "rpcUrl" {
1889+
*e = fmt_err!("vm.{name}: {e}");
1890+
}
1891+
}
1892+
}
18661893
trace!(
18671894
target: "cheatcodes",
18681895
return = %match result {

crates/cheatcodes/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,15 @@ pub(crate) trait Cheatcode: CheatcodeDef + DynCheatcode {
8585
}
8686

8787
pub(crate) trait DynCheatcode {
88+
fn name(&self) -> &'static str;
8889
fn id(&self) -> &'static str;
8990
fn as_debug(&self) -> &dyn std::fmt::Debug;
9091
}
9192

9293
impl<T: Cheatcode> DynCheatcode for T {
94+
fn name(&self) -> &'static str {
95+
T::CHEATCODE.func.signature.split('(').next().unwrap()
96+
}
9397
fn id(&self) -> &'static str {
9498
T::CHEATCODE.func.id
9599
}

crates/cheatcodes/src/string.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,7 @@ fn parse_value_fallback(s: &str, ty: &DynSolType) -> Option<Result<DynSolValue,
186186
"0" => false,
187187
s if s.eq_ignore_ascii_case("true") => true,
188188
s if s.eq_ignore_ascii_case("false") => false,
189-
_ => {
190-
return None;
191-
}
189+
_ => return None,
192190
};
193191
return Some(Ok(DynSolValue::Bool(b)));
194192
}

crates/cheatcodes/src/test/expect.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ fn expect_revert(
542542
}
543543

544544
pub(crate) fn handle_expect_revert(
545+
is_cheatcode: bool,
545546
is_create: bool,
546547
expected_revert: Option<&[u8]>,
547548
status: InstructionResult,
@@ -578,7 +579,9 @@ pub(crate) fn handle_expect_revert(
578579
}
579580
}
580581

581-
if actual_revert == expected_revert {
582+
if actual_revert == expected_revert ||
583+
(is_cheatcode && memchr::memmem::find(&actual_revert, expected_revert).is_some())
584+
{
582585
Ok(success_return())
583586
} else {
584587
let stringify = |data: &[u8]| {

0 commit comments

Comments
 (0)