This proposal introduces the milli-gas counter as the EVM’s internal gas accounting. Gas costs are defined in milli_gas and internal EVM gas accounting is entirely carried out in milli_gas. At the end of transaction execution, milli_gas is rounded up to gas. Gas limits and transaction fees are still computed and verified using the current gas_used counter. This new counter enables a more precise accounting of cheap compute without impacting UX.
Motivation
Currently, most EVM compute operations (ADD, SUB, MUL, etc.) are significantly underpriced when compared with state operations (SSTORE, SLOAD, CREATE, etc.). There are two factors contributing to this mismatch. On one hand, client optimizations and hardware improvements have made pure compute operations more efficient. On the other hand, with the growing size of the Ethereum state, the performance of I/O operations touching this larger state has been deteriorating. Both of these trends are expected to continue, further worsening this mismatch.
There are a couple of proposals to reduce the cost of compute operations (EIP-7904) and increase the cost of state operations (EIP-8032, EIP-8037, and EIP-8038). However, given the current level of mismatch between these operations, there is an inherent trade-off in the anchor we use to reprice these operations.
If we make compute operations cheaper, we will introduce rounding errors, with most compute operations costing 1, when they should cost a fraction of that. These rounding errors impact scalability because we can now fit fewer of these cheaper operations in any given block. However, if we price all operations so that the fastest opcode is assigned 1 gas, we would need to make all other operations significantly more expensive, including both compute and state operations. Making everything more expensive would require a rebase on the concept of gas units to users, thus impacting UX.
To illustrate this point, let’s focus on the pure compute operations. As compute operations impact only block execution time, the chosen anchor will induce a specific million gas per second rate on all compute opcodes. For instance, if we anchor on EcRecover at the current price (3000 gas units), this induces a rate of 175Mgas/s, based on the execution time estimated for this opcode on EIP-7904’s empirical analysis. The following plot shows the average rounding errors of compute operations for different anchors, using the same execution time estimates from EIP-7904.
As we can see, with an anchor of 50Mgas/s (which would result in a gas limit of 100Mgas, assuming a 2-second block execution limit), we would get an average rounding error of 59.3%. This means that we could be 59.3% cheaper on average per opcode if we didn’t have to round to integer gas units. Only after a 400Mgas/s anchor do we get to reasonably low rounding errors. However, this level of Mgas/s would require a major rebase of the gas model, where all opcodes become significantly more expensive, and both the transaction-level and block-level gas limits increase by a factor of 100.
The way around this trade-off is to allow cheap opcodes to have fractional gas costs, thus avoiding both the scalability limits introduced by rounding errors and the significant UX change of increasing gas costs and limits 100x.
A similar argument was made in EIP-2045, which served as inspiration to this proposal.
Specification
This proposal introduces a new Uint counter to the EVM, milli_gas_left. Gas costs are defined in milli_gas and internal EVM gas accounting is entirely carried out in milli_gas.
Transaction fees, transaction gas limits, and block gas limits are still computed and verified using the current gas_used counter. There are no changes to blob gas accounting, refunds, or call data gas accounting.
EVM environment
The current gas_left variable from the internal state of the EVM is replaced by milli_gas_left:
@dataclassclassEvm:"""The internal state of the virtual machine."""pc:Uintstack:List[U256]memory:bytearraycode:Bytesmilli_gas_left:Uintvalid_jump_destinations:Set[Uint]logs:Tuple[Log,...]refund_counter:intrunning:boolmessage:Messageoutput:Bytesaccounts_to_delete:Set[Address]return_data:Byteserror:Optional[EthereumException]accessed_addresses:Set[Address]accessed_storage_keys:Set[Tuple[Address,Bytes32]]
All references to evm.gas_left are replaced by evm.milli_gas_left.
Start-of-transaction conversion
The top-level frame starts with milli_gas_left = gas_left * 1000, where gas_left is the execution-gas budget that EIP-8037 computes at the start of the transaction (see Interaction with EIP-8037). For a transaction without a reservoir, this is (tx.gas - intrinsic_gas) * 1000. Intrinsic gas, the calldata floor and all other transaction-level quantities stay in gas units.
End-of-transaction conversion
At the end of each transaction execution, the gas left in the transaction output is rounded down, so that the gas used is rounded up:
tx_output.gas_left=evm.milli_gas_left//1000
The gas used before refunds is then tx.gas - tx_output.gas_left, as before this proposal. A transaction pays for less than one gas of rounding. The refund cap is computed from this rounded gas used.
Opcode costs
Opcode gas parameters as defined in the Ethereum Execution Client Specifications that are not updated at the same time or after the introduction of this proposal, are increased by 1000x when they are charged against milli_gas_left:
GAS_PARAM = GAS_PARAM * 1000
This includes memory expansion, precompile costs and GAS_CALL_STIPEND. A cost formula with a division, for example the memory expansion cost 3 × words + words² // 512 or the cost of MODEXP, is evaluated in gas units as before this proposal, and its result is multiplied by 1000. The formula then gives the same whole number of gas as before. A schedule that reprices such a formula states it in milli_gas.
The refund counter stays in gas units: refund parameters are not multiplied, and the refund cap applies to gas units, as before this proposal. A refund that a schedule derives from a cost in milli_gas, for example the refund of a write cost, is stated in gas units, rounded down.
Gas that contracts observe
Contracts observe gas in gas units, as before this proposal. Only the internal accounting uses milli_gas.
GAS opcode
The GAS opcode pushes the available gas in gas units, rounded down:
push(evm.stack,U256(evm.milli_gas_left//1000))
*CALL and CREATE* opcodes
The gas argument of CALL, CALLCODE, DELEGATECALL and STATICCALL is in gas units. The rule of EIP-150 is applied to the whole gas of the available milli_gas:
extra_milli_gas is the cost of the call before the gas is forwarded (the base cost, the access cost, the value transfer, the new account and the memory expansion), in milli_gas, as extra_gas in the execution specifications. The minimum is taken before the multiplication, so a fixed-width counter does not overflow: the stack value has 256 bits.
CREATE and CREATE2 forward max_gas * 1000, with max_gas computed in the same way. The fraction of a gas in available stays in the calling frame. Thus the forwarded gas never decreases when available increases, and when available is a multiple of 1000, the child gets the same gas as before this proposal. The call stipend, GAS_CALL_STIPEND * 1000, is added to the child’s milli_gas as before. When a child frame returns, its unused milli_gas is returned to the parent exactly, with no rounding.
SSTORE stipend check
The check of EIP-2200 compares milli_gas_left with GAS_CALL_STIPEND * 1000. Thus a frame that has the 2,300-gas stipend and nothing else still cannot execute SSTORE.
Quantities derived from the gas of a frame
A rule that computes a value from the gas of a frame uses milli_gas / 1000 without rounding, and rounds down only the final value. For example, a per-frame limit of the form child_gas * a // b becomes child_milli_gas * a // (b * 1000). When child_milli_gas is a multiple of 1000, the value is the same as before this proposal. The gas that a call or a create forwards, including the call stipend, is always a whole number of gas, so for a child frame the value is always the same as before this proposal.
Interaction with EIP-8037
milli_gas_left replaces the gas_left of EIP-8037. The other counters of EIP-8037 stay in gas units: state_gas_reservoir, evm_state_gas_used and the snapshots of both. The amount of each state-gas charge stays in gas.
The split at the start of the transaction is computed in gas, and gas_left is then converted:
The charges of the pre-execution phase of EIP-2780 are amounts in gas. The parts that gas_left pays are deducted from milli_gas_left, multiplied by 1000. If the transaction fails before the top-level frame starts, milli_gas_left is set to 0.
A state-gas charge is paid as EIP-8037 specifies. Every amount that EIP-8037 deducts from gas_left or compares with gas_left, such as a legacy share or the part that the reservoir does not pay, is multiplied by 1000 and applied to milli_gas_left. Any limit that EIP-8037 puts on what a frame takes from the reservoir, and the counters of that limit, stay in gas units.
Refills, reverts and exceptional halts follow EIP-8037 without change. A refill and the credit of a revert go to state_gas_reservoir, in gas, and never to milli_gas_left. An exceptional halt consumes the frame’s milli_gas_left. GAS and the SSTORE stipend check do not include the reservoir, as in EIP-8037.
At the end of the transaction, tx_output.gas_left = evm.milli_gas_left // 1000, and all other rules of EIP-8037 apply in gas units. evm_state_gas_used is an exact amount of gas, so the rounding goes only to the execution-gas dimension.
A system call starts with milli_gas_left = 30_000_000 * 1000 and the state_gas_reservoir of EIP-8037, in gas.
Interaction with frame transactions
When EIP-8141 is active, each frame of a frame transaction starts with milli_gas_left = frame.limits.execution * 1000. Its state-gas pool stays in gas units. When the frame completes, its gas_left is rounded down:
The frame’s execution gas used is therefore rounded up, once per frame. The receipt, FRAMEPARAM, and the settlement of EIP-8141 use this whole number of gas. A frame transaction pays less than one gas of rounding for each of its frames.
Rationale
New counter vs. new variable type
A possible approach to allow for fractional gas would be to change the variable type of all gas-related variables from an integer to a float or a base fractional. Although simple in concept, the implementation of such a change in the EVM is not trivial. In addition, introducing fractional variables would make moving to a zkEVM significantly more complex.
Thus, introducing a new counter is the option that least disrupts the current EVM implementation, while allowing for expressive gas costs.
Rounding at transaction-level
With the introduction of a new counter, there are two levels at which we could convert milli_gas to gas: at the transaction level or the block level. Conversion at the block level results in the fewest rounding errors in block utilization. However, it requires a bigger change to implement, as milli_gas will need to be used when computing transaction fees and limits, and milli_gas will need to be incorporated in the block environment.
The question is whether a conversion at the transaction level is sufficient to reduce rounding errors. To estimate this, we extracted the opcode counts per transaction for 1000 mainnet blocks and computed the new gas prices of all compute operations assuming an anchor of 50Mgas/s and the estimated execution times from EIP-7904’s empirical analysis. With this repricing, the fastest opcode (JUMPDEST) would be priced at 90 milli-gas units (i.e., 0.09 gas units). We also assumed that the non-compute operations and precompiles would have the same price, and we excluded any other transaction costs, such as intrinsic costs and calldata.
Then, we computed the rounding error of each transaction, assuming a conversion at both the transaction and opcode levels. The opcode-level conversion is equivalent to not using fractional gas and rounding all opcodes to an integer. The following plot shows the distribution for both errors. The x-axis is on a logarithmic scale.
In this sample of 1000 blocks, a transaction-level conversion has already successfully reduced the rounding errors from an average of 4% to 0.05% per transaction.
Factor of 1000
The factor sets the smallest step of a cost: one milli-gas. The cheapest price in the analysis above, 90 milli-gas for JUMPDEST at an anchor of 50 Mgas/s, is exact with this step. A target price is rounded to the nearest milli-gas, so its error is at most half a milli-gas, which is at most 1% for every price of at least 50 milli-gas. Thus the factor of 1000 is sufficient for schedules whose cheapest price is at least 0.05 gas, for example a further reduction of about 2 times from that anchor.
A schedule with cheaper prices needs a larger factor, for example 10,000. Such a factor also fits 64-bit counters: the transaction gas limit of EIP-7825 is about 1.7 × 10^11 units with it. A change of the factor changes every gas table, so the factor must be chosen together with the first schedule that uses fractional prices.
Gas units at the contract boundary
Deployed contracts use gas in three ways: they read GAS, they pass a gas argument to calls, and they rely on the 2,300-gas stipend. If GAS returned milli_gas while call arguments stayed in gas units, these uses would disagree. For example, a contract that forwards gasleft() - 5000 would keep a reserve of 5 gas instead of 5,000. A check such as gasleft() < gasRequired / 63 would become 1000 times looser, and a failure handler could then record an out-of-gas failure as a logic error.
So all three stay in gas units, and only the internal counter changes. A contract can then see a difference only where a cost changes. If a schedule only lowers costs, every gas budget that was sufficient for an execution path before this proposal stays sufficient for the same path. This is the safe direction for repricing. It does not hold for code whose control flow depends on the gas it reads. For example, code that reads GAS after cheap operations and enters a loop when the value is above a threshold can take the loop, and run out of gas, after a decrease.
GAS rounds down, so a contract never sees more gas than it has. Two GAS reads around cheap operations can return the same value. The end-of-transaction conversion rounds the charge up, so no transaction gets fractional work for free.
State-gas of EIP-8037 stays in gas units
Every state-gas charge of EIP-8037 is a whole multiple of its cost per state byte, so milli_gas gives it no precision. In gas units, the reservoir, the state-gas used and the snapshots stay exact, and a value that a contract reads from the reservoir has the same unit as GAS. The only rounding is at the end of the transaction.
Backwards Compatibility
This is a backwards incompatible gas repricing that requires a scheduled network upgrade.
Contracts see gas in gas units, as before this proposal: GAS, the gas argument of calls and the call stipend keep their meaning. Without a change in costs, every charge is a whole number of gas and milli_gas_left is always a multiple of 1000, so a transaction has the same result and the same gas used as before this proposal. With costs in milli_gas, a contract sees only the cost changes themselves.
Tools that trace execution need to choose how to report fractional costs and gas. Tracers should report gas and costs in gas units, and can add the milli_gas values.
Test Cases
All values are in milli_gas unless shown as gas.
No cost change. With every cost at 1000 times its value before this proposal, every transaction has the same result and the same gas used as before this proposal.
GAS. With milli_gas_left = 2,999, GAS pushes 2.
Forwarding with a fraction. A frame has 100,500 available for a call, after the cost of the call, and forwards all of it. The child starts with 99,000 and its GAS pushes 99. The caller keeps 1,500. With 100,000 available, the child also starts with 99,000, as before this proposal.
Monotonic forwarding. With 6,399,999 available and with 6,400,000 available, the child starts with 6,300,000 in both cases.
Large gas argument. A call with the gas argument 2**256 - 1 and 100,000 available forwards 99,000.
SSTORE stipend check.SSTORE fails the check with milli_gas_left = 2,300,000 and passes it with milli_gas_left = 2,300,001.
Memory expansion. An expansion to 100 words costs (3 × 100 + 100² // 512) × 1000 = 319,000, not 300,000 + 100² × 1000 // 512 = 319,531.
End of the transaction. A transaction that ends with milli_gas_left = 1,999 has tx_output.gas_left = 1 gas.
EIP-8037 state-gas charge. The top-level frame has milli_gas_left = 50,000,500 and a reservoir of 50,000 gas, and creates a new slot (97,920 gas, legacy share 10,000 gas). milli_gas_left pays 10,000,000, the reservoir pays 50,000 gas, and milli_gas_left pays the remaining 37,920,000. After the charge, milli_gas_left = 2,080,500, the reservoir is 0, and GAS pushes 2,080.
EIP-8141 frame. A frame with an execution limit of 1,000 gas that uses 100,100 has gas_used.execution = 101 gas.
Security Considerations
Lowering costs, which this proposal makes possible, has effects that contracts can observe:
A failure handler whose cost is mostly compute becomes cheaper, so a handler that ran out of gas before a decrease can complete after it. Handlers that write storage or emit logs are dominated by operations whose cost this proposal does not change. A later schedule that lowers those costs has the same effect on such handlers.
A loop that runs until gasleft() is low runs more iterations. The work per transaction and per block stays limited by the gas limits, which the repricing sets so that gas follows execution time.
Code whose control flow depends on the gas that it reads can take a different path after a decrease, and that path can run out of gas. A decrease keeps a budget sufficient only for the same execution path.
A callee with only the 2,300-gas stipend can run more compute. It still cannot write storage (the check of EIP-2200), and the costs of calls and logs do not change.
When compute costs go down, each transaction uses less gas, so more transactions fit in a block. Each of these transactions creates as much state as before, so the share of block gas spent on state creation increases. This proposal does not limit state growth. With EIP-8037, the state-gas dimension limits it, whatever the execution-gas costs.
The rounding of GAS cannot give a contract more gas than it has, and the end-of-transaction rounding cannot give a transaction free work. The rounding takes less than one gas per transaction, or per frame of an EIP-8141 transaction, so the rounding of a block is less than one gas for each transaction or frame in it.
A cost below one gas is correct only if its measurement includes the cost of each instruction that does not depend on the operation: dispatch, gas metering, and tracing. Cheap operations run many more times in a block, so this cost dominates them. The factor of 1000 does not change what a block can do. Only the costs do, and they are only as good as their measurements.
Costs set from execution time do not follow proving cost. For loop-heavy code, the proving cost per gas increases when compute costs go down. A chain whose blocks are limited by proving needs its own cost schedule.