Update EIP-8037: clarify reservoir mechanics#11328
Update EIP-8037: clarify reservoir mechanics#11328spencer-tb wants to merge 3 commits intoethereum:masterfrom
Conversation
File
|
EIPS/eip-8037.md
Outdated
| - The `GAS` opcode returns `gas_left` only (excluding the reservoir). | ||
| - The reservoir is passed **in full** to child frames (no 63/64 rule). Unused reservoir is returned to the parent on child completion. | ||
| - On **exceptional halt**, both `gas_left` and `state_gas_reservoir` are set to zero (all gas consumed), consistent with existing EVM out-of-gas semantics. This is not applied to system transactions. | ||
| - The reservoir is passed **in full** to child frames (no 63/64 rule). On child **revert**, unused `state_gas_reservoir` is returned to the parent. On child **exceptional halt**, `state_gas_reservoir` is zeroed and nothing is returned. |
There was a problem hiding this comment.
@rakita had a comment on this that I think we should think through.
When a CALL (or DELEGATECALL/CALLCODE) is made, the caller specifies a single gas value g. With multidim metereing, we need to figure out three things:
- Forwarding: How much of
gas_leftandreservoirdoes the subcall receive? - Success: How are unused portions returned?
- Failure: What happens on REVERT vs. exceptional halt (OOG)?
Here is a map of some options and their pros and cons.
Option 1: Forward all reservoir; return reservoir on any failure (this is the proposal from @rakita )
Mechanics:
- Subcall receives:
call_gas_left = min(g, 63/64 * caller.gas_left),call_reservoir = caller.reservoir(all of it) - On success: return unused
call_gas_leftand unusedcall_reservoirto caller - On REVERT: return unused
call_gas_leftand unusedcall_reservoirto caller - On exceptional halt: consume
call_gas_left(zeroed), return unusedcall_reservoirto caller
Rationale for returning reservoir on halt: State changes are reverted on failure, so no state was actually grown — the state gas wasn't "consumed" in any economic sense.
Pros:
- Subcalls have full access to state gas — large contract deployments via factories work without issues
- Reservoir is never "wasted" on failures, which is consistent with the principle that state gas pays for long-term state growth (which didn't happen)
- Simple forwarding logic (just pass it all through)
- Enables the full benefit of uncapped state gas in nested calls
Cons:
- The gas limit
gno longer bounds total spending — a subcall to an untrusted contract can consume unbounded state gas from the caller's reservoir - Breaks composability expectations: callers cannot limit how much state gas a subcall uses
- Existing patterns that use gas limits for economic protection (e.g., limiting gas sent to an external callback) become insufficient
- A malicious or buggy callee can drain the caller's entire reservoir even though the caller only intended to send
ggas
Option 2: Forward all reservoir; consume reservoir on exceptional halt (this is the current proposal)
Mechanics:
- Subcall receives:
call_gas_left = min(g, 63/64 * caller.gas_left),call_reservoir = caller.reservoir - On success: return unused
call_gas_leftand unusedcall_reservoir - On REVERT: return unused
call_gas_leftand unusedcall_reservoir - On exceptional halt: consume both
call_gas_leftandcall_reservoir(zeroed)
Pros:
- Consistent with current EVM behavior where exceptional halts consume all forwarded gas
- Provides a strong penalty for OOG, discouraging underestimation of gas
- Full reservoir access for subcalls
Cons:
- Extremely punishing — a single OOG in any subcall wipes the entire transaction's remaining state budget
- Makes calling untrusted contracts very risky (they can force-burn your reservoir by triggering OOG)
- Same composability problem as Option 1 (caller can't bound subcall state gas usage)
- The punishment is arguably disproportionate since no state was actually grown
Option 3: Forward reservoir proportional to gas fraction; return reservoir on failure
Mechanics:
- Subcall receives:
call_gas_left = min(g, 63/64 * caller.gas_left),call_reservoir = caller.reservoir * (call_gas_left / caller.gas_left) - On success: return unused portions of both
- On REVERT: return unused portions of both
- On exceptional halt: consume
call_gas_left, return unusedcall_reservoir
Pros:
- The gas parameter
gprovides proportional control over both dimensions — sending half your gas sends half your reservoir - Better composability: callers retain reservoir proportional to gas they didn't forward
- More predictable total spending (bounded by ~2× the gas fraction sent)
- Callers can limit exposure to untrusted contracts
Cons:
- State-heavy subcalls may be starved of reservoir (e.g., a factory deploying a 24kB contract needs a high gas limit even if regular gas usage is low)
- The proportional split may not match actual needs — some calls are 90% state, others 0%
- Adds calculation complexity and potential rounding issues
- Nested calls compound the problem: at depth N, the available reservoir may be heavily fragmented
Option 4: Forward reservoir capped at g; return reservoir on failure
Mechanics:
- Subcall receives:
call_gas_left = min(g, 63/64 * caller.gas_left),call_reservoir = min(g, caller.reservoir) - On success: return unused portions of both
- On REVERT: return unused portions of both
- On exceptional halt: consume
call_gas_left, return unusedcall_reservoir
Pros:
- The gas parameter
gsets a clear bound: total spending in the subcall ≤ 2g (at mostgfrom each pool) - Symmetric and intuitive — "I'm sending
gworth of resources in each dimension" - Callers retain excess reservoir beyond
g - Reasonable composability: gas limit still gives meaningful spending control
- Subcalls get enough reservoir for most use cases (the gas limit is usually set high enough to cover the call's needs)
Cons:
- A subcall can still spend up to 2× the gas parameter
g(potentially surprising) - For very state-heavy operations, the cap may be too restrictive — e.g., deploying a large contract from a factory may need reservoir >> regular gas
- Somewhat arbitrary choice to cap at exactly
g(why not 2g? or g/2?) - Doesn't solve the problem of deploying contracts larger than
TX_MAX_GAS_LIMITworth of state gas from within a subcall
Summary
| Reservoir forwarded | Spending bound | Factory/nested state ops | Composability | |
|---|---|---|---|---|
| Option 1: Forward all, return on fail | All | Unbounded | Full support | Poor |
| Option 2: Forward all, consume on halt | All | Unbounded | Full support | Very poor |
| Option 3: Proportional forward | Proportional | ~2× fraction | Partial | Good |
Option 4: Capped at g |
min(g, reservoir) | ≤ 2g | Partial | Good |
The fundamental tension is between reservoir accessibility in subcalls (needed for factory patterns and nested contract deployments) and caller control over spending (needed for composability with untrusted code). Options 1-2 maximize accessibility but sacrifice control while Options 3-4 attempt a middle ground. Either way, I think option 2 is too punishing and agree with @rakita that option 1 > option 2. The question is whether we want to add more control to caller gas spending by having option 3 or 4.
A future option to consider is introduce an explicit two-parameter call through a new opcode.
Mechanics:
- Introduce new CALL variants (e.g.,
CALL2D) or extend EOF call instructions to take two gas parameters:regular_gasandstate_gas - Subcall receives:
call_gas_left = regular_gas,call_reservoir = state_gas - Legacy CALL opcodes use a default policy (e.g., Option 3 or 4) for backward compatibility
- On success/REVERT/halt: handle each dimension independently per the chosen policy
Pros:
- Most explicit and flexible — callers have full control over both dimensions
- No ambiguity about behavior; no surprising interactions
- Can be introduced alongside EOF without breaking legacy contracts
- Future-proof: works cleanly if more dimensions are added later (per EIP-8011)
Cons:
- Requires new opcodes or changes to the EVM instruction set
- Legacy contracts cannot benefit — only new contracts using the new opcodes
- Increases EVM complexity (more opcodes, more edge cases)
- EOF adoption timeline is uncertain; legacy contracts may dominate for years
- Two gas parameters add UX complexity for developers and tooling
There was a problem hiding this comment.
From the testing perspective I do prefer Option 1 over 3/4, even with the greifing vector. Its a simple forward everything, return everything on failure which makes correctness easy to verify across success/revert paths, i.e a minimal test matrix. 3/4 seems more complex to implement and harder to catch all edge cases, more likely to introduce bugs. We could always introduce 3/4 in a future EIP if the need arises.
There was a problem hiding this comment.
Ok, sounds good. Let's go with option 1 for now and try to think about potential issues with this design. We can come back to this decision if I find significant issues.
There was a problem hiding this comment.
@spencer-tb, can you update the description to align with option 1?
There was a problem hiding this comment.
Added just now. Updated in the EELS PR as well. It helps with some of the test fails 🙏
Clarify EIP-8037 reservoir mechanics and fix minor issues identified during EELS implementation.
Remove stray
<-TODO->, document calldata floor interaction with refunds, clarifyrefund_counteris a single undimensioned counter and that EIP-7702 authorization refund goes tostate_gas_reservoir.ethereum/execution-specs#2181