Did you know that EVM has the functionality of rewarding users with a gas refund for clearing the on-chain storage occupied by contract data? Let’s learn more about this. There have been some changes after the London fork, but to understand it better we will look at the scenario before London Upgrade and then we will see the changes proposed in the London upgrade.
But before starting with refunds we should get a bit of understanding about storage gas consumption; this will make gas calculations easy to understand.
I have copied some gas amounts directly from the Ethereum Yellow paper, let’s see what are they:
1. Gcoldsload — 2,100 Cost of a cold storage access.
2. Gsset — 20,000 Paid for an SSTORE operation when the storage value is set to non-zero from zero.
3. Gsreset — 2,900 Paid for an SSTORE operation when the storage value’s zeroness remains unchanged or is set to zero.
4. Rsclear — 15,000 Refund given (added into refund counter) when the storage value is set to zero from non-zero.
Let’s quickly understand this:
1. Gcoldsload — you have to pay every time you access any storage variable for the first time in a function, the second or consecutive times it costs 100 gas.
2. Gsset — Pay every time you set any variable from zero value to non-zero (false to true in case of bools). In simple words, you are changing the default value and the nodes now have to keep track of that slot.
3. Gsreset — Pay every time you set a non-zero value to a non-zero or zero value.
4. Rsclear — Whenever you set any value to its default value you get the refund.
Remember these points before we start
Whenever we set any value from non-zero to non-zero or non-zero to zero, we collectively say the gas consumed to be 5,000, adding Gcoldsload and Gsreset.
Every transaction will consume all the related gas while executed and the refund is calculated at the very end of the transaction.
There is an initial gas attached to every transaction which is 21,000. This gas is consumed by the validator to determine whether the transaction is valid or not. 21,000 is added to every transaction and it is not in our control, so every transaction consumes 21,000 + whatever gas the function data needs.
The gas calculations below will implicitly include 21,000 gas.
The refunds work whenever you set something to its default value. For uint, it is zero; for a boolean, it’s false, etc. For ease of understanding, I’ll be taking examples of uint.
The EVM refunds gas to the user in two cases. First, if you are calling the selfdestruct
function, 24,000
gas is refunded from the total consumed gas. This was simple and there’s nothing to talk about as the gas refund functionality for selfdestruct
was removed in EIP-3529 and recently the selfdestruct
method is also deprecated.
Second, if you set any variable to its default value, you get a refund of 15,000. Why does it happen? Setting the value to default value means you are clearing the storage because the nodes don’t need to track that particular slot of storage you just cleared.
This is a screenshot of the Ethereum Yellow paper stating the refund amount.
But there is a proper way of calculating the gas to refund.
Here is the reference from the Ethereum Yellow paper.
Mechanism: The refund is given at the end of any transaction and capped at a maximum number. That maximum number is half of the total used gas.
Let’s understand it with the example of uint. What happens when we set any non-zero uint value to zero?
Suppose you are setting a single uint variable to zero in a transaction so the total applicable refund will be 15,000
, but the total gas consumed in this transaction is 24,000
, half of which is 12,000
, now 12,000
becomes the maximum amount of gas that can be refunded which means the transaction will now cost you24,000 — MAXIMUM_GAS_REFUNDABLE = 24000–12000 = 12000
gas.
Although we were expecting to get 15,000
gas back, the limit got capped at 12,000
, hence reducing the amount to 12,000
. What if our transaction is consuming 40,000
gas? In this case, half of 40,000
is 20,000
which is greater than 15,000
so we will get a refund of 15,000
gas.
This was the case when you are setting only one uint variable to zero, what will happen if we are setting multiple uint variables to zero? For example:
contract {
uint256 count = 1;
uint256 count2 = 2;
uint256 count3 = 3;
uint256 count4 = 3;
uint256 count5 = 3;
uint256 count6 = 5;
uint256 count7 = 6;
uint256 count8 = 6;
function setTOzero() external {
count = 0;
count2 = 0;
count3 = 0;
count4 = 0;
count5 = 0;
count6 = 0;
count7 = 0;
count8 = 0;
}
}
//Transaction cost will be 21000+ execution cost
//execution cost = 8 * (Gcoldsload +Gsreset) = 8 * 5000 = 40000
// Transaction cost = 21000 + 40000 = 61000
Whatever your transaction cost is the EXECUTION_COST+ 21,000
. Now the refund is calculated based on how many variables you are setting to default. In this case, we are setting 8 uint variables to zero so the refundable amount counts to be 15,000 * 8 = 120,000
.
Wait, if this is the refund amount then how much gas is our transaction consuming? When you calculate the gas to be consumed you will get the amount of approx 61,000
. Now, what will happen if 120,000
gas is refunded on a transaction of 61,000
? The miner will end up paying for the transaction, this is why the capping mechanism was introduced.
According to the formula, the maximum gas to be refunded will be 61000/2 = 30500
. So the gas consumed will be 61000–30500 = 30500
instead of 61000 – 1,20,000
. Now I think this is pretty clear to you, but this method is no more valid after the London upgrade.
If you want to test this then you can switch to the Berlin version of VM in Remix IDE and watch everything working.
Post London Upgrade
These numbers have changed after the introduction of EIP-3529 in the London upgrade. The refund gas amount is now reduced to 4,800
and the maximum amount that can be refunded now is one-fifth of the gas consumed. Let’s understand with the same approach as before.
In the first case, if we are setting only one variable to zero, the transaction is consuming 26,000
. when we notice the maximum gas rule in EIP-3529, it says TOTAL_GAS_CONSUMED divided by 5, which is in our case 26,000/5
equals 5,200
, which means we were eligible for a maximum refund of 5,200
in this transaction but as mentioned in the EIP-3529 the refund amount is 4,800
. This time 4,800
is refunded to us and the transaction costs 2,1400 gas
(some additional gas).
Let’s take the second case where we were setting 8 variables to zero. The gas consumed is still 61,000
. Talking about the refund, we expect a total refund of 4,800 * 8 =38,400
, meaning the consumption we expect is 61000–38400 = 22600
.
But when you execute this transaction, you will notice that the gas consumed is 49,000
. Now you understand that the gas to be refunded is capped to a maximum of 61,000/5 = 12,200
which means the transaction will consume a total of 61,000–12,200 ~ 49,000
.
You might be thinking what caused the gas refunds to get lower in the London upgrade. Look at the reasons I have copied from the official EIP website:
Gas refunds for _SSTORE_
and _SELFDESTRUCT_
were originally introduced to motivate application developers to write applications that practice “good state hygiene”, clearing storage slots and contracts that are no longer needed. However, the benefits of this technique have proven to be far lower than anticipated, and gas refunds have had multiple unexpected harmful consequences:
The concept of gas refund must be clear to you now. The more you play with this on remix, the more you will understand the calculations. Why not try executing some real-world functions as the ones I wrote above are just examples? You can try to see the gas consumption in real contracts like the transfer function ERC20 when the balance is about to get zero. Let me know what you find by these experiments.
Have any questions about this topic? Ask me anytime, on Twitter, LinkedIn, or telegram.
Also published here.