Skip to content

Conversation

grandizzy
Copy link
Collaborator

@grandizzy grandizzy commented Sep 10, 2025

Motivation

Solution

  • when broadcast, determine if it is not fixed gas limit by checking that the opcode before CALL is GAS;
  • on cheatcode inspector step record if we see GAS opcode; on step_end (where we have access to next opcode) set fixed gas limit to false if CALL

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

@grandizzy
Copy link
Collaborator Author

@klkvr @zerosnacks pls check and lmk if it makes sense, thank you!

@grandizzy grandizzy force-pushed the issue-11579 branch 2 times, most recently from f00cdf5 to 4898340 Compare September 11, 2025 09:37
@grandizzy grandizzy changed the title fix(forge): account gas limit per fork when script simulate fix(forge): determine if broadcasted tx is fixed gas limit using opcodes Sep 11, 2025
#[cold]
fn record_gas_limit_opcode(&mut self, interpreter: &mut Interpreter) {
if interpreter.bytecode.opcode() == op::GAS {
self.is_fixed_gas_limit = Some(true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can also change and set this to Some(false) and in set_gas_limit_type if none and next opcode is CALL then set to Some(true)

        if interpreter.bytecode.opcode() == op::CALL {
            if self.is_fixed_gas_limit.is_none() {
                // If GAS opcode wasn't seen then mark as fixed gas limit.
                self.is_fixed_gas_limit = Some(true);
            }
        }

@grandizzy grandizzy marked this pull request as ready for review September 11, 2025 15:58
Comment on lines +1087 to +1089
if self.broadcast.is_some() {
self.record_gas_limit_opcode(interpreter);
}
Copy link
Member

@klkvr klkvr Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe right now if opcode sequence would be GAS ...any sequence of opcodes other than CALL and GAS.. CALL we will still record this as non-fixed gas limit

what we could do is instead call record_gas_limit_opcode in step_end hook, and then in step either unset it or keep if the opcode is CALL

Copy link
Collaborator Author

@grandizzy grandizzy Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented in ba1215c but kept GAS followed by CALL check as it seems to me better logic. I updated to track Option<gas_seen, call_seen> and reset on step_end if GAS is not followed by CALL, pls check

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed it also solves #10968

Comment on lines 2334 to 2340
// 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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stillf find it a bit hard to understand. isn't step_end only called after call has completed meaning that this check will likely happen too late?

Copy link
Collaborator Author

@grandizzy grandizzy Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be wrong but afaik instep_end we already have access to the next opcode but before call, @DaniPopes @rakita could you pls chime in?

Copy link
Collaborator Author

@grandizzy grandizzy Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@klkvr I think I got your comment wrong, so want to go through logic again

  • in step we record if gas opcode. If it is a CALL then gas seen is false and will be interpreted as gas limit in call.
  • call happens, we check if gas was seen before and call preceded (that is both set to true). We reset the seen recordings if broadcast.
  • in step_end we check if next opcode is CALL and set no gas limit (Here we have access to next opcode )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah so in step_end interpreter.bytecode.opcode() is already the next opcode?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, learnt this when added the arbitrary storage cheatcode which changes target address storage if next opcode is SLOAD

/// Generates or copies arbitrary values for storage slots.
/// Invoked in inspector `step_end` (when the current opcode is not executed), if current opcode
/// to execute is `SLOAD` and storage slot is cold.
/// Ensures that in next step (when `SLOAD` opcode is executed) an arbitrary value is returned:
/// - copies the existing arbitrary storage value (or the new generated one if no value in
/// cache) from mapped source address to the target address.
/// - generates arbitrary value and saves it in target address storage.
#[cold]
fn arbitrary_storage_end(&mut self, interpreter: &mut Interpreter, ecx: Ecx) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that makes sense, the instruction pointer is advanced right before executing the instruction itself https://github.com/bluealloy/revm/blob/66e45930905b5c4b505dd328a716384d18eaa689/crates/interpreter/src/interpreter.rs#L286

klkvr
klkvr previously approved these changes Sep 12, 2025
Copy link
Member

@klkvr klkvr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, I believe the logic here is correct

left a nitpick re flag structure but can skip as the change is tiny

Comment on lines 502 to 505
/// 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)>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this only really has 3 state None, Some((true, true)) and Some((true, false)). worth checking if we can get rid of e.g one of the bools and/or remove the option entirely

also iiuc is_fixed_gas_limit: Some((true, true)) corresponds to actually a non-fixed gas limit which is a bit tricky to read, so maybe worth changing name/semantics of the flag

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I renamed to dynamic_gas_limit_sequence and updated description, hope that clarifies it a little bit. Re option, I still think we need it as the state can be reset to None on both when consuming in call, and also if next opcode is not a CALL... pending one more review to see others opinion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
3 participants