This EIP introduces a mechanism to dynamically price SSTORE operations based on the storage size of a contract. A new optional depth field is added to the account RLP, which tracks an approximation of the storage trie's depth. The gas cost for SSTORE will be augmented by a factor that grows exponentially with this depth field, but only after it crosses a predefined activation threshold. This change aims to align the cost of state growth with the long-term burden it places on the network, thereby disincentivizing state bloat.
Ethereum's state size is a growing concern, as it directly impacts node synchronization times, hardware requirements, and overall network health. The current gas model for storage operations does not fully account for the long-term cost of maintaining that state indefinitely. This has led to a situation where it is economically viable to create contracts with vast amounts of storage ("state bloat"), which can be used for low-cost data anchoring or spam, imposing a negative externality on all network participants who must maintain this data.
This proposal aims to address the state growth problem by creating a direct economic link between the size of a contract's storage and the cost to expand it further. By using the trie depth as a proxy for the total storage size, we can introduce a progressive gas fee for SSTORE. This ensures that small-to-medium-sized contracts are unaffected, while contracts that contribute disproportionately to state growth face increasing costs for storage writes. This creates a market-based incentive for developers to be more efficient with their use of state and helps mitigate unsustainable growth patterns.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Name | Value | Description |
---|---|---|
FORK_TIMESTAMP |
TBD | Fork activation timestamp |
EXP_FACTOR |
TBD | Exponent gas cost factor |
LIN_FACTOR |
TBD | Linear gas cost factor |
ACTIVATION_THRESHOLD |
TBD | Activation threshold, chosen to be at ~8GB of data. |
Account RLP descriptors receive an optional depth
field, corresponding to the maximum depth written to since FORK_TIMESTAMP
.
After block execution and while computing the state root for the account, take note of the maximum depth written to.
While updating the account's root hash, also update its depth
fields according to the following rules:
```python= newroot, max_depth = compute_root_and_max_depth() if account.depth < max_depth: account.depth += 1
account.root = new_root
where `compute_root_and_max_depth` is a function that writes all storage changes to the tree, and return the maximum depth that a storage slot has been written to during that process.
### Gas cost changes
At the beginning of a block, when an account is loaded, its `depth` field is available.
The gas cost of an `SSTORE` is computed as such:
```python=
constant_sstore_gas(addr, slot) + LIN_FACTOR * (1 << (4 * EXP_FACTOR * account.depth)) // ACTIVATION_THRESHOLD
This adds 1 byte per account, as the maximum depth of the MPT is 64, which is lower than 128 and therefore fits in a single RLP byte.
There are currently about 23 million accounts in the state, so we are looking at a current state growth of about 23 MB. This is a high bound, and since our research has shown that most of the accounts are currently inactive, we expect no negative impact as long as the number of new accounts itself isn't a problem.
The intent is to create friction in when growing the state size of a contract, thus limiting the number of such contracts. Going over the limit, some contract developers might want to use another contract to start fresh, which comes at the cost of paying for contract creation, and for any call into the previous instance of the contract.
The depth of the tree is chosen over the leaf count, because the storage tree is balanced, owing to the fact that its keys are hashed. Therefore, checking the maximum depth of a write is a stochastic process that on average, will return the size of the tree.
The increase by one at each block is meant to mitigate the effects of an unbalanced tree in case of an "unlucky" contract having a very deep branch while being mostly empty: writes then become more expensive over time. On the other hand, spam accounts that see frequent writes, will see the price increase kick in after just a few blocks.
The depth is chosen as a proxy for the total leaf count, as there is a logarithmic relationship between the two in a balanced tree.
ACTIVATION_THRESHOLD
is chosen to not penalize the contracts that are large, but do provide useful value. The idea is to disincentivize spam contracts who grow larger than useful contracts, that are legitimately big due to the fact they bring value to their users and the wider ecosystem. This means that this constant could be increased as the state grows.
No backward compatibility issues found. Making the depth
field optional ensures that the default depth is 0, which is fine since it will be updated on the first write, and not updating it marks that contract as a state expiry target.
This is a backwards incompatible gas repricing that requires a scheduled network upgrade.
Node operators MUST update gas estimation handling to accommodate the new calldata cost rules. Specifically, RPC methods such as eth_estimateGas
MUST incorporate the updated formula for gas calculation when encountering an SSTORE
.
Users and wallets can maintain their usual workflows without modification, as RPC updates will handle these changes.
Name | Description |
---|---|
Activation check | Create a tree with an account that has no depth value and a storage slot at depth > ACTIVATION_TARGET . Write to that slot, and verify that depth is then set to 1 . Depending on the value of ACTIVATION_TARGET , this might require a deep collision, so reducing that value for the test is doable. |
Threshold check | Inverse of the previous test: same with a shallow tree, ensure that the value doesn't increase. |
Monotonous increase | Run the same test as "Activation Check" but across several blocks in order to confirm that it grows by one each time. |
TODO
Needs discussion.
A remark was made that, were many contracts updated to include a depth
field in a single block, this would represent writes to about 3k accounts. This could put stress on client databases. Since this change only applies to large contracts, the risk of this happening is already mitigated by the cost of filling the storage of such contracts.
Copyright and related rights waived via CC0.