Part I of this series introduced the Ethereum Virtual Machine (EVM), introduced the stack, opcodes, and gave some simplistic examples using the PUSH0, PUSH1, and ADD instructions.
A smart contract is capable of more than simple stack manipulation and arithmetic. To really make good use of the EVM, we need to implement more advanced techniques.
This lesson will cover how to implement control logic like if-else
, if-elif-else
, while
, and for
. These familiar building blocks allow us to control the execution flow through our smart contract.
But first we have to review the two instructions that glue it all together.
JUMP / JUMPI
Programmers using high level languages are used to calling functions within others. It’s easy to visualize the execution of a program that uses function calls. Inside func_1, you pause execution at some point, begin the execution of func_2, then resume where func_1 left off.
Smart contracts give the illusion that EVM has functions. But there’s no such thing as a function, not really. It’s all just a stream of bytes, a program counter, and simple actions done with a stack (🤫 I’ll cover memory and storage later).
At the lowest level, sophisticated control flow is just a series of boolean checks followed by JUMPs.
If you’ve programmed C, you may remember learning about the GOTO statement. GOTO allows you to move the execution of the current function somewhere else within that function. GOTO is part of the C language, but you must never use it because it’s … icky?
If that bummed you out, don’t fret — JUMP is GOTO for the EVM, but nobody gets mad when you use it!
Recall that the program counter (PC) is updated to the specific offset in the program code where the current opcode is located. JUMP modifies the PC, setting it to a new value. Then the execution resumes at that offset — with one exception: the opcode at the destination must be 0x5B (JUMPDEST). If any other opcode is found, EVM will revert. This ensures that execution doesn’t resume in an area that might lead to corruption. Of course, the responsibility is always on the programmer to ensure that the actions taken after the JUMPDEST are sane.
JUMP takes one value: the new program counter.
JUMPI operates similarly but takes two values: the new PC and a check value — if the check value is non-zero, it behaves just like JUMP. Otherwise the PC advances to the next opcode as normal.
To illustrate this, we can build a very simple mnemonic contract in the evm.codes Playground. I’ve included the PC values for each instruction, which will appear in the right side. Note that PUSH1 is at PC=0 and JUMP is at PC=2 because of the need to place the 0x0b value in the contract where PC=1 would go.
[0x00] PUSH1 0x0b // 11
[0x02] JUMP
[0x03] PUSH0
[0x04] PUSH0
[0x05] PUSH0
[0x06] PUSH0
[0x07] PUSH0
[0x08] PUSH0
[0x09] PUSH0
[0x0a] PUSH0
[0x0b] JUMPDEST
[0x0c] PUSH1 0x45 // 69
This silly code immediately JUMPs to the JUMPDEST on line 11 (0x0b), skipping over a bunch of PUSH0 instructions that would otherwise fill up the stack. Then it performs the last instruction — pushing 69 onto the stack (nice).
You should get this same result in the Playground.
Now modify it to use JUMPI by placing a control value on the stack first, then the PC:
[0x00] PUSH1 0x01 // 1
[0x02] PUSH1 0x0b // 11
[0x04] JUMPI
[0x05] PUSH0
[0x06] PUSH0
[0x07] PUSH0
[0x08] PUSH0
[0x09] PUSH0
[0x0a] PUSH0
[0x0b] JUMPDEST
[0x0c] PUSH1 0x45 // 69
I have removed one PUSH0 instruction to preserve the same JUMPDEST target. But this code accomplishes the same result. JUMPI executes, skipping the PUSH0 instructions and we end with 0x45 on the stack.
What if the check value for JUMPI is zero?
[0x00] PUSH0
[0x01] PUSH1 0x0b // 11
[0x03] JUMPI
[0x04] PUSH0
[0x05] PUSH0
[0x06] PUSH0
[0x07] PUSH0
[0x08] PUSH0
[0x09] PUSH0
[0x10] PUSH0
[0x11] JUMPDEST
[0x12] PUSH1 0x45 // 69
Here, the JUMPI instruction does not resolve to move to line 11. Instead, it moves to the next instruction at PC=4. Then seven zeros are pushed onto the stack. Then JUMPDEST executes without effect (other than consuming gas). Then 69 is pushed as before.
The final result of this code should be this stack: [69, 0, 0, 0, 0, 0, 0, 0].
If-Else
With the ability to shift execution of the program depending on conditionals, we can implement a simple if-else check.
Keep reading with a 7-day free trial
Subscribe to Degen Code to keep reading this post and get 7 days of free access to the full post archives.