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.
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.
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), orGAS_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.state
original_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
if account != original_account:
gas_cost += GAS_WARM_ACCOUNT_WRITE_COST
else:
gas_cost += GAS_COLD_ACCOUNT_WRITE_COST
# Charge slot-specific costs
if (evm.message.current_target, key) not in evm.accessed_storage_keys:
evm.accessed_storage_keys.add((evm.message.current_target, key))
gas_cost += GAS_COLD_SLOAD
if original_value == current_value and current_value != new_value:
if original_value == 0:
gas_cost += GAS_STORAGE_SET
else:
gas_cost += GAS_COLD_STORAGE_WRITE - GAS_COLD_SLOAD
else:
gas_cost += GAS_WARM_ACCESS
# Refund Counter Calculation
if current_value != new_value:
if original_value != 0 and current_value != 0 and new_value == 0:
# Storage is cleared for the first time in the transaction
evm.refund_counter += int(GAS_STORAGE_CLEAR_REFUND)
if original_value != 0 and current_value == 0:
# Gas refund issued earlier to be reversed
evm.refund_counter -= int(GAS_STORAGE_CLEAR_REFUND)
if original_value == new_value:
# Storage slot being restored to its original value
if original_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)
if account == 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.
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.
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 (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.
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:
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.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.
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.
Copyright and related rights waived via CC0.