---
eip: 1283
title: Net gas metering for SSTORE without dirty maps
author: Wei Tang (@sorpaas)
discussions-to: https://github.com/sorpaas/EIPs/issues/1
status: Final
type: Standards Track
category: Core
created: 2018-08-01
---

## Abstract

This EIP proposes net gas metering changes for `SSTORE` opcode, enabling
new usages for contract storage, and reducing excessive gas costs
where it doesn't match how most implementation works.

This acts as an alternative for EIP-1087, where it tries to be
friendlier to implementations that use different optimization
strategies for storage change caches.

## Motivation

This EIP proposes a way for gas metering on SSTORE (as an alternative
for EIP-1087 and EIP-1153), using information that is more universally
available to most implementations, and require as little change in
implementation structures as possible.

* *Storage slot's original value*.
* *Storage slot's current value*. 
* Refund counter.

Usages that benefits from this EIP's gas reduction scheme includes:

* Subsequent storage write operations within the same call frame. This
  includes reentry locks, same-contract multi-send, etc.
* Exchange storage information between sub call frame and parent call
  frame, where this information does not need to be persistent outside
  of a transaction. This includes sub-frame error codes and message
  passing, etc.

## Specification

Definitions of terms are as below:

* *Storage slot's original value*: This is the value of the storage if
  a reversion happens on the *current transaction*.
* *Storage slot's current value*: This is the value of the storage
  before SSTORE operation happens.
* *Storage slot's new value*: This is the value of the storage after
  SSTORE operation happens.

Replace `SSTORE` opcode gas cost calculation (including refunds) with
the following logic:

* If *current value* equals *new value* (this is a no-op), 200 gas is
  deducted.
* If *current value* does not equal *new value*
  * If *original value* equals *current value* (this storage slot has
    not been changed by the current execution context)
    * If *original value* is 0, 20000 gas is deducted.
    * Otherwise, 5000 gas is deducted. If *new value* is 0, add 15000
      gas to refund counter.
  * If *original value* does not equal *current value* (this storage
    slot is dirty), 200 gas is deducted. Apply both of the following
    clauses.
    * If *original value* is not 0
      * If *current value* is 0 (also means that *new value* is not
        0), remove 15000 gas from refund counter. We can prove that
        refund counter will never go below 0.
      * If *new value* is 0 (also means that *current value* is not
        0), add 15000 gas to refund counter.
    * If *original value* equals *new value* (this storage slot is
      reset)
      * If *original value* is 0, add 19800 gas to refund counter.
      * Otherwise, add 4800 gas to refund counter.

Refund counter works as before -- it is limited to half of the gas
consumed. On a transaction level, refund counter will never go below
zero. However, there are some important notes depending on the
implementation details:

* If an implementation uses "transaction level" refund counter (refund
  is checkpointed at each call frame), then the refund counter
  continues to be unsigned.
* If an implementation uses "execution-frame level" refund counter
  (a new refund counter is created at each call frame, and then merged
  back to parent when the call frame finishes), then the refund
  counter needs to be changed to signed -- at internal calls, a child
  refund can go below zero.

## Explanation

The new gas cost scheme for `SSTORE` is divided into three different
types:

* **No-op**: the virtual machine does not need to do anything. This is
  the case if *current value* equals *new value*.
* **Fresh**: this storage slot has not been changed, or has been reset
  to its original value. This is the case if *current value* does not
  equal *new value*, and *original value* equals *current value*.
* **Dirty**: this storage slot has already been changed. This is the
  case if *current value* does not equal *new value*, and *original
  value* does not equal *current value*.

We can see that the above three types cover all possible variations of
*original value*, *current value*, and *new value*.

**No-op** is a trivial operation. Below we only consider cases for
**Fresh** and **Dirty**.

All initial (not-**No-op**) `SSTORE` on a particular storage slot starts
with **Fresh**. After that, it will become **Dirty** if the value has
been changed. When going from **Fresh** to **Dirty**, we charge the
gas cost the same as current scheme. A **Dirty** storage slot can be
reset back to **Fresh** via a `SSTORE` opcode. This will trigger a
refund.

When a storage slot remains at **Dirty**, we charge 200 gas. In this
case, we would also need to keep track of `R_SCLEAR` refunds -- if we
already issued the refund but it no longer applies (*current value* is
0), then removes this refund from the refund counter. If we didn't
issue the refund but it applies now (*new value* is 0), then adds this
refund to the refund counter. It is not possible where a refund is not
issued but we remove the refund in the above case, because all storage
slot starts with **Fresh** state.

### State Transition

Below is a graph ([by
@Arachnid](https://github.com/ethereum/EIPs/pull/1283#issuecomment-410229053))
showing possible state transition of gas costs. We ignore **No-op**
state because that is trivial:

![State Transition](../assets/eip-1283/state.png)

Below is table version of the above diagram. Vertical shows the *new
value* being set, and horizontal shows the state of *original value*
and *current value*.

When *original value* is 0:

|    | A (`current=orig=0`) | B (`current!=orig`)      |
|----|----------------------|--------------------------|
| ~0 | B; 20k gas           | B; 200 gas               |
| 0  | A; 200 gas           | A; 200 gas, 19.8k refund |

When *original value* is not 0:

|             | X (`current=orig!=0`) | Y (`current!=orig`)     | Z (`current=0`)           |
|-------------|-----------------------|-------------------------|---------------------------|
| `orig`      | X; 200 gas            | X; 200 gas, 4.8k refund | X; 200 gas, -10.2k refund |
| `~orig, ~0` | Y; 5k gas             | Y; 200 gas              | Y; 200 gas, -15k refund   |
| 0           | Z; 5k gas, 15k refund | Z; 200 gas, 15k refund  | Z; 200 gas                |

## Rationale

This EIP mostly achieves what a transient storage tries to do
(EIP-1087 and EIP-1153), but without the complexity of introducing the
concept of "dirty maps", or an extra storage struct.

* We don't suffer from the optimization limitation of
  EIP-1087. EIP-1087 requires keeping a dirty map for storage changes,
  and implicitly makes the assumption that a transaction's storage
  changes are committed to the storage trie at the end of a
  transaction. This works well for some implementations, but not for
  others. After EIP-658, an efficient storage cache implementation
  would probably use an in-memory trie (without RLP encoding/decoding)
  or other immutable data structures to keep track of storage changes,
  and only commit changes at the end of a block. For them, it is
  possible to know a storage's original value and current value, but
  it is not possible to iterate over all storage changes without
  incurring additional memory or processing costs.
* It never costs more gas compared with the current scheme.
* It covers all usages for a transient storage. Clients that are easy
  to implement EIP-1087 will also be easy to implement this
  specification. Some other clients might require a little bit extra
  refactoring on this. Nonetheless, no extra memory or processing cost
  is needed on runtime.

Regarding `SSTORE` gas cost and refunds, see Appendix for proofs of
properties that this EIP satisfies.

* For *absolute gas used* (that is, actual *gas used* minus *refund*),
  this EIP is equivalent to EIP-1087 for all cases.
* For one particular case, where a storage slot is changed, reset to
  its original value, and then changed again, EIP-1283 would move more
  gases to refund counter compared with EIP-1087.

Examine examples provided in EIP-1087's Motivation:

* If a contract with empty storage sets slot 0 to 1, then back to 0,
  it will be charged `20000 + 200 - 19800 = 400` gas.
* A contract with empty storage that increments slot 0 5 times will be
  charged `20000 + 5 * 200 = 21000` gas.
* A balance transfer from account A to account B followed by a
  transfer from B to C, with all accounts having nonzero starting and
  ending balances, it will cost `5000 * 3 + 200 - 4800 = 10400` gas.

## Backwards Compatibility

This EIP requires a hard fork to implement. No gas cost increase is
anticipated, and many contracts will see gas reduction.

## Test Cases

Below we provide 17 test cases. 15 of them covering consecutive two
`SSTORE` operations are based on work [by
@chfast](https://github.com/ethereum/tests/issues/483). Two additional
case with three `SSTORE` operations is used to test the case when a
slot is reset and then set again.

| Code                               | Used Gas | Refund | Original | 1st | 2nd | 3rd |
|------------------------------------|----------|--------|----------|-----|-----|-----|
| `0x60006000556000600055`           | 412      | 0      | 0        | 0   | 0   |     |
| `0x60006000556001600055`           | 20212    | 0      | 0        | 0   | 1   |     |
| `0x60016000556000600055`           | 20212    | 19800  | 0        | 1   | 0   |     |
| `0x60016000556002600055`           | 20212    | 0      | 0        | 1   | 2   |     |
| `0x60016000556001600055`           | 20212    | 0      | 0        | 1   | 1   |     |
| `0x60006000556000600055`           | 5212     | 15000  | 1        | 0   | 0   |     |
| `0x60006000556001600055`           | 5212     | 4800   | 1        | 0   | 1   |     |
| `0x60006000556002600055`           | 5212     | 0      | 1        | 0   | 2   |     |
| `0x60026000556000600055`           | 5212     | 15000  | 1        | 2   | 0   |     |
| `0x60026000556003600055`           | 5212     | 0      | 1        | 2   | 3   |     |
| `0x60026000556001600055`           | 5212     | 4800   | 1        | 2   | 1   |     |
| `0x60026000556002600055`           | 5212     | 0      | 1        | 2   | 2   |     |
| `0x60016000556000600055`           | 5212     | 15000  | 1        | 1   | 0   |     |
| `0x60016000556002600055`           | 5212     | 0      | 1        | 1   | 2   |     |
| `0x60016000556001600055`           | 412      | 0      | 1        | 1   | 1   |     |
| `0x600160005560006000556001600055` | 40218    | 19800  | 0        | 1   | 0   | 1   |
| `0x600060005560016000556000600055` | 10218    | 19800  | 1        | 0   | 1   | 0   |

## Appendix: Proof

Because the *storage slot's original value* is defined as the value
when a reversion happens on the *current transaction*, it's easy to
see that call frames won't interfere SSTORE gas calculation. So
although the below proof is discussed without call frames, it applies
to all situations with call frames. We will discuss the case
separately for *original value* being zero and not zero, and use
*induction* to prove some properties of SSTORE gas cost.

*Final value* is the value of a particular storage slot at the end of
a transaction. *Absolute gas used* is the absolute value of *gas used*
minus *refund*. We use `N` to represent the total number of SSTORE
operations on a storage slot. For states discussed below, refer to
*State Transition* in *Explanation* section.

### Original Value Being Zero

When *original value* is 0, we want to prove that:

* **Case I**: If the *final value* ends up still being 0, we want to charge `200 *
  N` gases, because no disk write is needed.
* **Case II**: If the *final value* ends up being a non-zero value, we want to
  charge `20000 + 200 * (N-1)` gas, because it requires writing this
  slot to disk.
  
#### Base Case

We always start at state A. The first SSTORE can:

* Go to state A: 200 gas is deducted. We satisfy *Case I* because
  `200 * N == 200 * 1`.
* Go to state B: 20000 gas is deducted. We satisfy *Case II* because
  `20000 + 200 * (N-1) == 20000 + 200 * 0`.
  
#### Inductive Step

* From A to A. The previous gas cost is `200 * (N-1)`. The current
  gas cost is `200 + 200 * (N-1)`. It satisfy *Case I*.
* From A to B. The previous gas cost is `200 * (N-1)`. The current
  gas cost is `20000 + 200 * (N-1)`. It satisfy *Case II*.
* From B to B. The previous gas cost is `20000 + 200 * (N-2)`. The
  current gas cost is `200 + 20000 + 200 * (N-2)`. It satisfy
  *Case II*.
* From B to A. The previous gas cost is `20000 + 200 * (N-2)`. The
  current gas cost is `200 - 19800 + 20000 + 200 * (N-2)`. It satisfy
  *Case I*.
  
### Original Value Not Being Zero

When *original value* is not 0, we want to prove that:

* **Case I**: If the *final value* ends up unchanged, we want to
  charge `200 * N` gases, because no disk write is needed.
* **Case II**: If the *final value* ends up being zero, we want to
  charge `5000 - 15000 + 200 * (N-1)` gas. Note that `15000` is the
  refund in actual definition.
* **Case III**: If the *final value* ends up being a changed non-zero
  value, we want to charge `5000 + 200 * (N-1)` gas.
  
#### Base Case

We always start at state X. The first SSTORE can:

* Go to state X: 200 gas is deducted. We satisfy *Case I* because
  `200 * N == 200 * 1`.
* Go to state Y: 5000 gas is deducted. We satisfy *Case III* because
  `5000 + 200 * (N-1) == 5000 + 200 * 0`.
* Go to state Z: The absolute gas used is `5000 - 15000` where 15000
  is the refund. We satisfy *Case II* because `5000 - 15000 + 200 *
  (N-1) == 5000 - 15000 + 200 * 0`.
  
#### Inductive Step

* From X to X. The previous gas cost is `200 * (N-1)`. The current gas
  cost is `200 + 200 * (N-1)`. It satisfy *Case I*.
* From X to Y. The previous gas cost is `200 * (N-1)`. The current gas
  cost is `5000 + 200 * (N-1)`. It satisfy *Case III*.
* From X to Z. The previous gas cost is `200 * (N-1)`. The current
  absolute gas cost is `5000 - 15000 + 200 * (N-1)`. It satisfy *Case
  II*.
* From Y to X. The previous gas cost is `5000 + 200 * (N-2)`. The
  absolute current gas cost is `200 - 4800 + 5000 + 200 * (N-2)`. It
  satisfy *Case I*.
* From Y to Y. The previous gas cost is `5000 + 200 * (N-2)`. The
  current gas cost is `200 + 5000 + 200 * (N-2)`. It satisfy *Case
  III*.
* From Y to Z. The previous gas cost is `5000 + 200 * (N-2)`. The
  current absolute gas cost is `200 - 15000 + 5000 + 200 * (N-2)`. It
  satisfy *Case II*.
* From Z to X. The previous gas cost is `5000 - 15000 + 200 *
  (N-2)`. The current absolute gas cost is `200 + 10200 + 5000 -
  15000 + 200 * (N-2)`. It satisfy *Case I*.
* From Z to Y. The previous gas cost is `5000 - 15000 + 200 *
  (N-2)`. The current absolute gas cost is `200 + 15000 + 5000 -
  15000 + 200 * (N-2)`. It satisfy *Case III*.
* From Z to Z. The previous gas cost is `5000 - 15000 + 200 *
  (N-2)`. The current absolute gas cost is `200 + 5000 - 15000 + 200 *
  (N-2)`. It satisfy *Case II*.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
