PUSH0 opcode: A significant update in the latest solidity version 0.8.20
Solidity just released its newest version of Solidity, 0.8.20.
And, as always, along with it comes quite a few new changes, improvements, bug fixes, etc.
However, there is one imperative update in this version that you as a smart contract developer must be aware of.
That is - The inclusion of a new opcode called PUSH0
.
A quick important note from the Solidity team’s official announcement blog on PUSH0
:
The new compiler switches the default target EVM version to Shanghai, which means that the generated bytecode will include PUSH0
opcodes.
First things first, what exactly is PUSH0?
PUSH0
opcode is actually fairly simple.
It is an instruction with just one specific job, i.e., to push the constant value ZERO onto the stack. That’s it, trust me.
Although PUSH0
has recently been included with solidity version 0.8.20, its significance has been quite evident since 2021, with the inception of EIP-3855.
In the following sections, we will delve deeper into the significance of this seemingly simple opcode and explore its crucial implications.
What we had so far...
If you take a quick look at the opcodes related to PUSH operations, that we had so far (Instruction 0x60 to 0x7f), you will realize we had opcodes from PUSH1 to PUSH32.
This technically means, we had ways to PUSH any item from 1 byte to 32 bytes onto the stack by using the respective opcode as per our need.
PUSH1 = 0x60, // Place 1 byte item on stack.
PUSH2 = 0x61, // Place 2 byte item on stack.
PUSH3 = 0x62, // Place 3 byte item on stack.
PUSH4 = 0x63, // Place 4 byte item on stack.
PUSH5 = 0x64, // Place 5 byte item on stack.
PUSH6 = 0x65, // Place 6 byte item on stack.
PUSH7 = 0x66, // Place 7 byte item on stack.
PUSH8 = 0x67, // Place 8 byte item on stack.
PUSH9 = 0x68, // Place 9 byte item on stack.
PUSH10 = 0x69, // Place 10 byte item on stack.
....
PUSH31 = 0x7e, // Place 31 byte item on stack.
PUSH32 = 0x7f, // Place 32 byte item on stack.
However, what we didn’t have was an adequate way of pushing ZERO onto the stack.
Therefore, in order to push ZERO onto the stack, we had workarounds like:
- With “PUSH1 00”: using the PUSH1 opcode to push zeroes onto the stack,
- Using multiple DUP instructions to duplicate zeroes and put them on the stack, etc.
While the workarounds mentioned above did the job, they weren’t adequate enough.
Using "PUSH1 00", for instance, is encoded as two bytes and technically consumes more gas than it should put a zero on the stack.
And using multiple DUP instructions might inflate the contract code size and isn’t an optimized approach.
As previously mentioned, this new opcode might seem quite simple but it does solve all these issues that were being ignored for quite a long.
A part of the EIP3855 documentation says:
To put the “waste” into perspective, across existing accounts 340,557,331 bytes are wasted onPUSH1 00
instructions, which means 68,111,466,200 gas was spent to deploy them.
In practice, a lot of these accounts share identical bytecode with others, so their total stored size in clients is lower, however, the deploy time cost must have been paid nevertheless.
That’s some insane amount of gas wasted for just putting zeros on the stack. 🤯
Enters PUSH0 opcode
Now with the inclusion of PUSH0
, we shall be able to solve all these issues at once.
PUSH0 opcode shall allow us to directly push a constant 0 value onto the stack without the need to use multiple DUP instructions or a combination of other opcodes.
With the new PUSH0
opcode, we now have:
- Adequate mechanism of pushing zero onto the stack,
- Reduced contract bytecode size as we can replace quite a few opcodes with just PUSH0,
- Minimizing the use of DUP instructions for duplicating zeros on the stack, etc
Time to Verify
It's Web3 folks, so 👇
Don't trust, Verify.
Let's try to verify all of the above-mentioned details about PUSH0
opcode with an example and see if it actually does the magic. 🪄
We will take this small contract for example,
contract PushZero_Test{
uint256 public num;
function set(uint256 _n) public{
num = _n;
}
}
Then, let's Compile and Deploy this contract:
- First, use older versions like 0.8.19 or 0.8.16, etc.
- Then using the latest version, 0.8.20.
Results? 🤔
- Contract Code Size
➡️ Contract bytecode size using older version = ~678 characters
➡️ Contract bytecode size using 0.8.20 versions = ~646 characters - Deployment Gas Cost ⛽️
➡️ Contract Deployment cost using older versions = ~61511 gas
➡️ Contract Deployment cost using 0.8.20 versions = ~58909 gas
As a result of this short experiment, we can clearly verify that with the inclusion of PUSH0
opcode, we now have:
- Reduced contract code size, and
- Reduced contract deployment gas cost
Hmmm, the opcode does its magic well. 🧐
Don't skip the experiments.
Try and verify yourself too
☢️ Important Warning for Solidity Devs
Although PUSH0
opcode is now included with the latest solidity compiler, you need to be careful while using it on any other chain than ETH mainnet.
There are still other chains like L2s that don't recognize the PUSH0
opcode. Therefore, if you try to use the latest compiler to deploy your contract on any such chain that doesn't support this opcode, your contract deployment shall fail.
And you might get an error that looks something like this 👇
Therefore, in such cases, you must ensure that you have selected the correct EVM version. Learn more about selecting the right EVM version to target, HERE.
Alright decipherer, hope that was helpful.
See you later. 👋🏻