Skip to content

Commit 03e16d8

Browse files
authored
fix(forge): determine if broadcasted tx is fixed gas limit using opcodes (foundry-rs#11599)
* fix(forge): determine if fixed gas limit when simulate * Reset gas if next opcode is not CALL * Rename var
1 parent 5ad85f0 commit 03e16d8

File tree

1 file changed

+58
-18
lines changed

1 file changed

+58
-18
lines changed

crates/cheatcodes/src/inspector.rs

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,11 @@ pub struct Cheatcodes {
499499
pub wallets: Option<Wallets>,
500500
/// Signatures identifier for decoding events and functions
501501
pub signatures_identifier: Option<SignaturesIdentifier>,
502+
/// Used to determine whether the broadcasted call has non-fixed gas limit.
503+
/// Holds values for (seen opcode GAS, seen opcode CALL) pair.
504+
/// If GAS opcode is followed by CALL opcode then both flags are marked true and call
505+
/// has non-fixed gas limit, otherwise the call is considered to have fixed gas limit.
506+
pub dynamic_gas_limit_sequence: Option<(bool, bool)>,
502507
}
503508

504509
// This is not derived because calling this in `fn new` with `..Default::default()` creates a second
@@ -554,6 +559,7 @@ impl Cheatcodes {
554559
deprecated: Default::default(),
555560
wallets: Default::default(),
556561
signatures_identifier: SignaturesIdentifier::new(true).ok(),
562+
dynamic_gas_limit_sequence: Default::default(),
557563
}
558564
}
559565

@@ -852,8 +858,15 @@ impl Cheatcodes {
852858
});
853859
}
854860

855-
let is_fixed_gas_limit = check_if_fixed_gas_limit(&ecx, call.gas_limit);
856-
861+
let (gas_seen, call_seen) =
862+
self.dynamic_gas_limit_sequence.take().unwrap_or_default();
863+
// Transaction has fixed gas limit if no GAS opcode seen before CALL opcode.
864+
let mut is_fixed_gas_limit = !(gas_seen && call_seen);
865+
// Additional check as transfers in forge scripts seem to be estimated at 2300
866+
// by revm leading to "Intrinsic gas too low" failure when simulated on chain.
867+
if call.gas_limit < 21_000 {
868+
is_fixed_gas_limit = false;
869+
}
857870
let input = TransactionInput::new(call.input.bytes(ecx));
858871
// Ensure account is touched.
859872
ecx.journaled_state.touch(broadcast.new_origin);
@@ -1077,6 +1090,10 @@ impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
10771090
fn step(&mut self, interpreter: &mut Interpreter, ecx: Ecx) {
10781091
self.pc = interpreter.bytecode.pc();
10791092

1093+
if self.broadcast.is_some() {
1094+
self.record_gas_limit_opcode(interpreter);
1095+
}
1096+
10801097
// `pauseGasMetering`: pause / resume interpreter gas.
10811098
if self.gas_metering.paused {
10821099
self.meter_gas(interpreter);
@@ -1117,6 +1134,10 @@ impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
11171134
}
11181135

11191136
fn step_end(&mut self, interpreter: &mut Interpreter, ecx: Ecx) {
1137+
if self.broadcast.is_some() {
1138+
self.set_gas_limit_type(interpreter);
1139+
}
1140+
11201141
if self.gas_metering.paused {
11211142
self.meter_gas_end(interpreter);
11221143
}
@@ -1614,7 +1635,7 @@ impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
16141635

16151636
if curr_depth == broadcast.depth {
16161637
input.set_caller(broadcast.new_origin);
1617-
let is_fixed_gas_limit = check_if_fixed_gas_limit(&ecx, input.gas_limit());
1638+
16181639
// Ensure account is touched.
16191640
ecx.journaled_state.touch(broadcast.new_origin);
16201641

@@ -1627,7 +1648,6 @@ impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
16271648
value: Some(input.value()),
16281649
input: TransactionInput::new(input.init_code()),
16291650
nonce: Some(account.info.nonce),
1630-
gas: if is_fixed_gas_limit { Some(input.gas_limit()) } else { None },
16311651
..Default::default()
16321652
}
16331653
.into(),
@@ -2296,6 +2316,40 @@ impl Cheatcodes {
22962316
(REVERT, 0, 1, false),
22972317
);
22982318
}
2319+
2320+
#[cold]
2321+
fn record_gas_limit_opcode(&mut self, interpreter: &mut Interpreter) {
2322+
match interpreter.bytecode.opcode() {
2323+
// If current opcode is CREATE2 then set non-fixed gas limit.
2324+
op::CREATE2 => self.dynamic_gas_limit_sequence = Some((true, true)),
2325+
op::GAS => {
2326+
if self.dynamic_gas_limit_sequence.is_none() {
2327+
// If current opcode is GAS then mark as seen.
2328+
self.dynamic_gas_limit_sequence = Some((true, false));
2329+
}
2330+
}
2331+
_ => {}
2332+
}
2333+
}
2334+
2335+
#[cold]
2336+
fn set_gas_limit_type(&mut self, interpreter: &mut Interpreter) {
2337+
// Early exit in case we already determined is non-fixed gas limit.
2338+
if matches!(self.dynamic_gas_limit_sequence, Some((true, true))) {
2339+
return;
2340+
}
2341+
2342+
// Record CALL opcode if GAS opcode was seen.
2343+
if matches!(self.dynamic_gas_limit_sequence, Some((true, false)))
2344+
&& interpreter.bytecode.opcode() == op::CALL
2345+
{
2346+
self.dynamic_gas_limit_sequence = Some((true, true));
2347+
return;
2348+
}
2349+
2350+
// Reset dynamic gas limit sequence if GAS opcode was not followed by a CALL opcode.
2351+
self.dynamic_gas_limit_sequence = None;
2352+
}
22992353
}
23002354

23012355
/// Helper that expands memory, stores a revert string pertaining to a disallowed memory write,
@@ -2323,20 +2377,6 @@ fn disallowed_mem_write(
23232377
));
23242378
}
23252379

2326-
// Determines if the gas limit on a given call was manually set in the script and should therefore
2327-
// not be overwritten by later estimations
2328-
fn check_if_fixed_gas_limit(ecx: &Ecx, call_gas_limit: u64) -> bool {
2329-
// If the gas limit was not set in the source code it is set to the estimated gas left at the
2330-
// time of the call, which should be rather close to configured gas limit.
2331-
// TODO: Find a way to reliably make this determination.
2332-
// For example by generating it in the compilation or EVM simulation process
2333-
ecx.tx.gas_limit > ecx.block.gas_limit &&
2334-
call_gas_limit <= ecx.block.gas_limit
2335-
// Transfers in forge scripts seem to be estimated at 2300 by revm leading to "Intrinsic
2336-
// gas too low" failure when simulated on chain
2337-
&& call_gas_limit > 2300
2338-
}
2339-
23402380
/// Returns true if the kind of account access is a call.
23412381
fn access_is_call(kind: crate::Vm::AccountAccessKind) -> bool {
23422382
matches!(

0 commit comments

Comments
 (0)