From 1e06cefe8c2298588b40dc3f85607f56c87195b4 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Thu, 11 Sep 2025 12:13:28 +0300 Subject: [PATCH 1/3] fix(forge): determine if fixed gas limit when simulate --- crates/cheatcodes/src/inspector.rs | 59 ++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 7280a22991a46..6f87794db6fed 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -499,6 +499,10 @@ pub struct Cheatcodes { pub wallets: Option, /// Signatures identifier for decoding events and functions pub signatures_identifier: Option, + /// Determine if broadcasted call has fixed gas by tracking checking that CALL is preceded by + /// GAS opcode. Set to option true when GAS opcode seen and to option false if next opcode is + /// CALL. + pub is_fixed_gas_limit: Option, } // This is not derived because calling this in `fn new` with `..Default::default()` creates a second @@ -554,6 +558,7 @@ impl Cheatcodes { deprecated: Default::default(), wallets: Default::default(), signatures_identifier: SignaturesIdentifier::new(true).ok(), + is_fixed_gas_limit: Default::default(), } } @@ -852,8 +857,12 @@ impl Cheatcodes { }); } - let is_fixed_gas_limit = check_if_fixed_gas_limit(&ecx, call.gas_limit); - + let mut is_fixed_gas_limit = self.is_fixed_gas_limit.take().unwrap_or_default(); + // Additional check as transfers in forge scripts seem to be estimated at 2300 + // by revm leading to "Intrinsic gas too low" failure when simulated on chain. + if call.gas_limit < 21000 { + is_fixed_gas_limit = false; + }; let input = TransactionInput::new(call.input.bytes(ecx)); let account = @@ -1075,6 +1084,10 @@ impl Inspector> for Cheatcodes { fn step(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { self.pc = interpreter.bytecode.pc(); + if self.broadcast.is_some() { + self.record_gas_limit_opcode(interpreter); + } + // `pauseGasMetering`: pause / resume interpreter gas. if self.gas_metering.paused { self.meter_gas(interpreter); @@ -1115,6 +1128,10 @@ impl Inspector> for Cheatcodes { } fn step_end(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { + if self.broadcast.is_some() { + self.set_gas_limit_type(interpreter); + } + if self.gas_metering.paused { self.meter_gas_end(interpreter); } @@ -1612,8 +1629,6 @@ impl Inspector> for Cheatcodes { if curr_depth == broadcast.depth { input.set_caller(broadcast.new_origin); - let is_fixed_gas_limit = check_if_fixed_gas_limit(&ecx, input.gas_limit()); - let account = &ecx.journaled_state.inner.state()[&broadcast.new_origin]; self.broadcastable_transactions.push_back(BroadcastableTransaction { rpc: ecx.journaled_state.database.active_fork_url(), @@ -1623,7 +1638,6 @@ impl Inspector> for Cheatcodes { value: Some(input.value()), input: TransactionInput::new(input.init_code()), nonce: Some(account.info.nonce), - gas: if is_fixed_gas_limit { Some(input.gas_limit()) } else { None }, ..Default::default() } .into(), @@ -2292,6 +2306,27 @@ impl Cheatcodes { (REVERT, 0, 1, false), ); } + + #[cold] + fn record_gas_limit_opcode(&mut self, interpreter: &mut Interpreter) { + if interpreter.bytecode.opcode() == op::GAS { + self.is_fixed_gas_limit = Some(true); + } + } + + #[cold] + fn set_gas_limit_type(&mut self, interpreter: &mut Interpreter) { + if interpreter.bytecode.opcode() == op::CALL { + if self.is_fixed_gas_limit.is_some() { + // If GAS opcode was seen and current opcode is CALL then it doesn't have fixed gas + // limit. + self.is_fixed_gas_limit = Some(false); + } else { + // If GAS opcode wasn't seen then it has fixed call limit. + self.is_fixed_gas_limit = Some(true); + } + } + } } /// Helper that expands memory, stores a revert string pertaining to a disallowed memory write, @@ -2319,20 +2354,6 @@ fn disallowed_mem_write( )); } -// Determines if the gas limit on a given call was manually set in the script and should therefore -// not be overwritten by later estimations -fn check_if_fixed_gas_limit(ecx: &Ecx, call_gas_limit: u64) -> bool { - // If the gas limit was not set in the source code it is set to the estimated gas left at the - // time of the call, which should be rather close to configured gas limit. - // TODO: Find a way to reliably make this determination. - // For example by generating it in the compilation or EVM simulation process - ecx.tx.gas_limit > ecx.block.gas_limit && - call_gas_limit <= ecx.block.gas_limit - // Transfers in forge scripts seem to be estimated at 2300 by revm leading to "Intrinsic - // gas too low" failure when simulated on chain - && call_gas_limit > 2300 -} - /// Returns true if the kind of account access is a call. fn access_is_call(kind: crate::Vm::AccountAccessKind) -> bool { matches!( From ba1215ca52bca416d882c54dd708b88bd6f1f79a Mon Sep 17 00:00:00 2001 From: grandizzy Date: Fri, 12 Sep 2025 10:42:23 +0300 Subject: [PATCH 2/3] Reset gas if next opcode is not CALL --- crates/cheatcodes/src/inspector.rs | 51 +++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 6f87794db6fed..6154700eb1835 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -499,10 +499,10 @@ pub struct Cheatcodes { pub wallets: Option, /// Signatures identifier for decoding events and functions pub signatures_identifier: Option, - /// Determine if broadcasted call has fixed gas by tracking checking that CALL is preceded by - /// GAS opcode. Set to option true when GAS opcode seen and to option false if next opcode is - /// CALL. - pub is_fixed_gas_limit: Option, + /// Whether the broadcasted call has fixed gas limit (if no GAS opcode seen before + /// CALL opcode). Set to (true, true) when GAS is followed by a CALL opcode or if CREATE2 + /// opcode. + pub is_fixed_gas_limit: Option<(bool, bool)>, } // This is not derived because calling this in `fn new` with `..Default::default()` creates a second @@ -857,12 +857,14 @@ impl Cheatcodes { }); } - let mut is_fixed_gas_limit = self.is_fixed_gas_limit.take().unwrap_or_default(); + let (gas_seen, call_seen) = self.is_fixed_gas_limit.take().unwrap_or_default(); + // Transaction has fixed gas limit if no GAS opcode seen before CALL opcode. + let mut is_fixed_gas_limit = !(gas_seen && call_seen); // Additional check as transfers in forge scripts seem to be estimated at 2300 // by revm leading to "Intrinsic gas too low" failure when simulated on chain. - if call.gas_limit < 21000 { + if call.gas_limit < 21_000 { is_fixed_gas_limit = false; - }; + } let input = TransactionInput::new(call.input.bytes(ecx)); let account = @@ -2309,23 +2311,36 @@ impl Cheatcodes { #[cold] fn record_gas_limit_opcode(&mut self, interpreter: &mut Interpreter) { - if interpreter.bytecode.opcode() == op::GAS { - self.is_fixed_gas_limit = Some(true); + match interpreter.bytecode.opcode() { + // If current opcode is CREATE2 then set non-fixed gas limit. + op::CREATE2 => self.is_fixed_gas_limit = Some((true, true)), + op::GAS => { + if self.is_fixed_gas_limit.is_none() { + // If current opcode is GAS then mark as seen. + self.is_fixed_gas_limit = Some((true, false)); + } + } + _ => {} } } #[cold] fn set_gas_limit_type(&mut self, interpreter: &mut Interpreter) { - if interpreter.bytecode.opcode() == op::CALL { - if self.is_fixed_gas_limit.is_some() { - // If GAS opcode was seen and current opcode is CALL then it doesn't have fixed gas - // limit. - self.is_fixed_gas_limit = Some(false); - } else { - // If GAS opcode wasn't seen then it has fixed call limit. - self.is_fixed_gas_limit = Some(true); - } + // Early exit in case we already determined is non-fixed gas limit. + if matches!(self.is_fixed_gas_limit, Some((true, true))) { + return; } + + // Record CALL opcode if GAS opcode was seen. + if matches!(self.is_fixed_gas_limit, Some((true, false))) + && interpreter.bytecode.opcode() == op::CALL + { + self.is_fixed_gas_limit = Some((true, true)); + return; + } + + // Reset gas record if GAS opcode was not followed by a CALL opcode. + self.is_fixed_gas_limit = None; } } From f1634195e2834aa0a835fa858557829b2d25e53a Mon Sep 17 00:00:00 2001 From: grandizzy Date: Fri, 12 Sep 2025 15:33:36 +0300 Subject: [PATCH 3/3] Rename var --- crates/cheatcodes/src/inspector.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 6154700eb1835..f91ca04d59a06 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -499,10 +499,11 @@ pub struct Cheatcodes { pub wallets: Option, /// Signatures identifier for decoding events and functions pub signatures_identifier: Option, - /// Whether the broadcasted call has fixed gas limit (if no GAS opcode seen before - /// CALL opcode). Set to (true, true) when GAS is followed by a CALL opcode or if CREATE2 - /// opcode. - pub is_fixed_gas_limit: Option<(bool, bool)>, + /// Used to determine whether the broadcasted call has non-fixed gas limit. + /// Holds values for (seen opcode GAS, seen opcode CALL) pair. + /// If GAS opcode is followed by CALL opcode then both flags are marked true and call + /// has non-fixed gas limit, otherwise the call is considered to have fixed gas limit. + pub dynamic_gas_limit_sequence: Option<(bool, bool)>, } // This is not derived because calling this in `fn new` with `..Default::default()` creates a second @@ -558,7 +559,7 @@ impl Cheatcodes { deprecated: Default::default(), wallets: Default::default(), signatures_identifier: SignaturesIdentifier::new(true).ok(), - is_fixed_gas_limit: Default::default(), + dynamic_gas_limit_sequence: Default::default(), } } @@ -857,7 +858,8 @@ impl Cheatcodes { }); } - let (gas_seen, call_seen) = self.is_fixed_gas_limit.take().unwrap_or_default(); + let (gas_seen, call_seen) = + self.dynamic_gas_limit_sequence.take().unwrap_or_default(); // Transaction has fixed gas limit if no GAS opcode seen before CALL opcode. let mut is_fixed_gas_limit = !(gas_seen && call_seen); // Additional check as transfers in forge scripts seem to be estimated at 2300 @@ -2313,11 +2315,11 @@ impl Cheatcodes { fn record_gas_limit_opcode(&mut self, interpreter: &mut Interpreter) { match interpreter.bytecode.opcode() { // If current opcode is CREATE2 then set non-fixed gas limit. - op::CREATE2 => self.is_fixed_gas_limit = Some((true, true)), + op::CREATE2 => self.dynamic_gas_limit_sequence = Some((true, true)), op::GAS => { - if self.is_fixed_gas_limit.is_none() { + if self.dynamic_gas_limit_sequence.is_none() { // If current opcode is GAS then mark as seen. - self.is_fixed_gas_limit = Some((true, false)); + self.dynamic_gas_limit_sequence = Some((true, false)); } } _ => {} @@ -2327,20 +2329,20 @@ impl Cheatcodes { #[cold] fn set_gas_limit_type(&mut self, interpreter: &mut Interpreter) { // Early exit in case we already determined is non-fixed gas limit. - if matches!(self.is_fixed_gas_limit, Some((true, true))) { + if matches!(self.dynamic_gas_limit_sequence, Some((true, true))) { return; } // Record CALL opcode if GAS opcode was seen. - if matches!(self.is_fixed_gas_limit, Some((true, false))) + if matches!(self.dynamic_gas_limit_sequence, Some((true, false))) && interpreter.bytecode.opcode() == op::CALL { - self.is_fixed_gas_limit = Some((true, true)); + self.dynamic_gas_limit_sequence = Some((true, true)); return; } - // Reset gas record if GAS opcode was not followed by a CALL opcode. - self.is_fixed_gas_limit = None; + // Reset dynamic gas limit sequence if GAS opcode was not followed by a CALL opcode. + self.dynamic_gas_limit_sequence = None; } }