This EIP introduces warm metering for account writes. Namely, if one of the account fields (nonce, value, codehash) is changed more than once in a transaction, the later writes are cheaper, since the state root update only happens once.
Motivation
Updating the state root is one of the most expensive parts of block construction. Currently, multiple writes to storage are subject to a net gas metering, which reduces the cost of a storage write after the first write. However, updates to the account are subject to the same cost every time.
This means that, for example, making multiple WETH transfers to an account in a single transaction gets successfully cheaper as the cold access cost is amortized over the remaining accesses. At the same time, the same discount does not occur when making multiple native ETH transfers. This discourages people from using native ETH transfers, and unfairly penalizes potential future opcodes that involve value transfer, like PAY and GAS2ETH.
This EIP brings the gas cost of the account update more in line with the actual execution cost. Multiple writes within a transaction can be batched, meaning that, after the first write, the cost of updating the state root does not need to be charged again.
Specification
The parameters GAS_CALL_VALUE and GAS_STORAGE_UPDATE are removed, and the following parameters are introduced:
Parameter
Value
Description
GAS_COLD_STORAGE_WRITE
TBD
Cost of a single update to the storage trie
GAS_COLD_ACCOUNT_WRITE
TBD
Cost of a single update to the account trie
GAS_WARM_WRITE
TBD
Cost of a warm update to a trie (i.e. does not trigger a new state root calculation)
<– TODO –>
On the account-updating opcodes CREATE, CREATE2, and *CALL, instead of charging GAS_CALL_VALUE:
GAS_COLD_ACCOUNT_WRITE_COST is charged if the account fields are equal to the transaction start values (i.e., they have not yet been updated by the transaction), or
GAS_WARM_ACCOUNT_WRITE_COST is charged if the account fields are not equal to the transaction start values (i.e., they have already been updated before by the transaction).
SSTORE is also subjected to warm account metering. That is, this EIP splits the cost of SSTORE into two components, the cost to update the account tuple, and the cost to update the state trie. Note that if the state trie has already been updated once in a transaction, the account tuple is already dirty, and so we can amortize the cost of updating the account tuple again. Thus, the gas cost of SSTORE and its refund logic is updated to:
# get original_value and current_value
state=evm.message.block_env.stateoriginal_value=get_storage_original(state,evm.message.current_target,key)current_value=get_storage(state,evm.message.current_target,key)# get account info
original_account=get_account_original(evm.message.block_env.state,evm.message.current_target)account=get_account(evm.message.block_env.state,evm.message.current_target)# initialize gas cost
gas_cost=Uint(0)# Charge account write cost
ifaccount!=original_account:gas_cost+=GAS_WARM_ACCOUNT_WRITE_COSTelse:gas_cost+=GAS_COLD_ACCOUNT_WRITE_COST# Charge slot-specific costs
if(evm.message.current_target,key)notinevm.accessed_storage_keys:evm.accessed_storage_keys.add((evm.message.current_target,key))gas_cost+=GAS_COLD_SLOADiforiginal_value==current_valueandcurrent_value!=new_value:iforiginal_value==0:gas_cost+=GAS_STORAGE_SETelse:gas_cost+=GAS_COLD_STORAGE_WRITE-GAS_COLD_SLOADelse:gas_cost+=GAS_WARM_ACCESS# Refund Counter Calculation
ifcurrent_value!=new_value:iforiginal_value!=0andcurrent_value!=0andnew_value==0:# Storage is cleared for the first time in the transaction
evm.refund_counter+=int(GAS_STORAGE_CLEAR_REFUND)iforiginal_value!=0andcurrent_value==0:# Gas refund issued earlier to be reversed
evm.refund_counter-=int(GAS_STORAGE_CLEAR_REFUND)iforiginal_value==new_value:# Storage slot being restored to its original value
iforiginal_value==0:# Slot was originally empty and was SET earlier
evm.refund_counter+=int(GAS_STORAGE_SET-GAS_WARM_ACCESS)else:# Slot was originally non-empty and was UPDATED earlier
evm.refund_counter+=int(GAS_COLD_STORAGE_WRITE-GAS_COLD_SLOAD-GAS_WARM_ACCESS)# Refund account writting cost (only if cold)
ifaccount==original_account:evm.refund_counter+=GAS_COLD_ACCOUNT_WRITE_COST
For compatibility with EIP-7928 and parallel execution, if the accessed account shows updates in the Block-Level Access List (BAL) in a transaction indexed before the current transaction, then the values to compare against are taken from this entry instead of the account trie.
Rationale
An account is represented within Ethereum as a tuple (nonce, balance, storage_root, codehash). The account is a leaf of a Merkle Patricia Tree (MPT), while the storage_root is itself the root of the account’s MPT key-value store. An update to the account’s storage requires updating two MPTs (the account’s storage_root, as well as the global state root). Meanwhile, updating the other fields in an account requires updating only one MPT.
This proposal clarifies the cost of updating state by introducing the *_WRITE parameters. It also separates the cost of a storage state root update (GAS_COLD_STORAGE_ACCESS) and the cost of an account state root update (GAS_COLD_ACCOUNT_ACCESS). This parametrization allows us to apply warm costing to account updates. When the same account is updated multiple times in the same transaction, the state root calculation can be batched and all updates can be done in the same calculation. Therefore, the cost of making more updates to an already updated account is not the same as the first update.
Benchmarking
This proposal does not yet have finalized numbers. To achieve this, we require stateful benchmarks, which are currently in development. Once we collect that data, we will set the final numbers.
<– TODO –>
Net metering
Net metering (i.e., issuing a refund if the final value at the end of the transaction is equal to the transaction start, à la SSTORE) was considered, but not added for simplicity.
Backwards Compatibility
This is a backwards-incompatible gas repricing that requires a scheduled network upgrade.
Wallet developers and node operators MUST update gas estimation handling to accommodate the new account access cost rules. Specifically:
Wallets: Wallets using eth_estimateGas MUST be updated to ensure that they correctly account for the updated gas parameters. Failure to do so could result in overestimating gas, leading to potential attacks vectors.
Node Software: RPC methods such as eth_estimateGas MUST incorporate the updated formula for gas calculation with the new cost values.
Users can maintain their usual workflows without modification, as wallet and RPC updates will handle these changes.
Security Considerations
Decreasing the cost of account access operations could introduce new attack vectors. More analysis is needed to understand the potential effects on various dApps and user behaviors.