diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index aac2166ba4199..79011dc00bdb9 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -499,6 +499,11 @@ pub struct Cheatcodes { pub wallets: Option, /// Signatures identifier for decoding events and functions pub signatures_identifier: Option, + /// 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 @@ -554,6 +559,7 @@ impl Cheatcodes { deprecated: Default::default(), wallets: Default::default(), signatures_identifier: SignaturesIdentifier::new(true).ok(), + dynamic_gas_limit_sequence: Default::default(), } } @@ -852,8 +858,15 @@ impl Cheatcodes { }); } - let is_fixed_gas_limit = check_if_fixed_gas_limit(&ecx, call.gas_limit); - + 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 + // by revm leading to "Intrinsic gas too low" failure when simulated on chain. + if call.gas_limit < 21_000 { + is_fixed_gas_limit = false; + } let input = TransactionInput::new(call.input.bytes(ecx)); // Ensure account is touched. ecx.journaled_state.touch(broadcast.new_origin); @@ -1077,6 +1090,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); @@ -1117,6 +1134,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); } @@ -1614,7 +1635,7 @@ 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()); + // Ensure account is touched. ecx.journaled_state.touch(broadcast.new_origin); @@ -1627,7 +1648,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(), @@ -2296,6 +2316,40 @@ impl Cheatcodes { (REVERT, 0, 1, false), ); } + + #[cold] + 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.dynamic_gas_limit_sequence = Some((true, true)), + op::GAS => { + if self.dynamic_gas_limit_sequence.is_none() { + // If current opcode is GAS then mark as seen. + self.dynamic_gas_limit_sequence = Some((true, false)); + } + } + _ => {} + } + } + + #[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.dynamic_gas_limit_sequence, Some((true, true))) { + return; + } + + // Record CALL opcode if GAS opcode was seen. + if matches!(self.dynamic_gas_limit_sequence, Some((true, false))) + && interpreter.bytecode.opcode() == op::CALL + { + self.dynamic_gas_limit_sequence = Some((true, true)); + return; + } + + // Reset dynamic gas limit sequence if GAS opcode was not followed by a CALL opcode. + self.dynamic_gas_limit_sequence = None; + } } /// Helper that expands memory, stores a revert string pertaining to a disallowed memory write, @@ -2323,20 +2377,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!(