Cover photo

Control Flow in Bytecode

filosofiacodigo.eth

filosofiacodigo.eth and Cooldev1337

We reject abstraction
We believe in bytecode
Bytecode is only true form
Let’s code with bytes now

Last week, we talked about Stack, Memory, and Storage, the three data regions of the EVM. Today, we’re going to explore how smart contracts make decisions and change execution paths using control flow.

What is control flow?

Control flow is the logic that determines what happens next in a program. In Solidity, you use if, else, for, while, or function calls. In bytecode, those high-level keywords are translated into jump instructions.

The EVM is a very simple machine, it executes bytecode one instruction at a time, moving forward by default. But with control flow instructions, we can tell it to jump to a different part of the code, or conditionally jump based on a value.

The Core Control Flow Opcodes

Opcode

Name

Description

56

JUMP

Jump to a specific position in the code (unconditional)

57

JUMPI

Jump to a position only if a condition is met (conditional)

5B

JUMPDEST

Marks a valid place to jump to. You must jump only to a location with this opcode

Example: Conditional return based on input

Let’s say you want to write a smart contract that:

  • Returns 0x02 if the input is greater than 10

  • Otherwise, returns 0x01

This is what our example looks like in pseudocode:

if (input > 10) {
  return 0x02;
} else {
  return 0x01;
}

And here’s what the EVM bytecode can be implemented:

Offset

Bytecode

Description

00

60 0A

PUSH1 0x0A Push the number 10 to the stack

02

60 00

PUSH1 0x00 We are going to load the first 32 bytes of calldata (the program input)

04

35

CALLDATALOAD Load the number sent as parameter

05

11

GT Check if input > 10

06

60 0E

PUSH1 0x0E Destination if TRUE

08

57

JUMPI Jump to label if GT was TRUE

False branch (default: input <= 10)

09

60 01

PUSH1 0x01 Push value 1

0B

60 12

PUSH1 0x12 Destination to branch merge point

0D

56

JUMP to merge point

True branch (input > 10) starts at 0x12

0E

5B

JUMPDEST Safe jump target

0F

60 02

PUSH1 0x02 Push value 2

Branch merge point, true and false branches converge here

12

5B

JUMPDEST Safe jump target

13

60 00

PUSH1 0x00 Memory offset 0

15

52

MSTORE Store the result (either the number 1 or 2) from the stack to memory location 0

16

60 20

PUSH1 0x20 Return length set to 32 bytes

18

60 00

PUSH1 0x00 Return offset set to 0

1A

F3

RETURN 32 bytes and end execution

post image
Step-by-step execution when passed 15 (0x00..0E) as calldata parameter. This number is greater than 10 to 0x02 will be returned.
post image
5 is lesser than 10 so 0x01 is returned

Do we need JUMPDEST?

The EVM doesn’t allow jumping to just any byte, it has to be a JUMPDEST. This design prevents jumping into the middle of data or invalid instruction segments. Think of JUMPDEST as a checkpoint you can land on.

While marking safe spots for jump instructions is considered a sound and secure virtual machine design choice, the EVM has been rightfully criticized for requiring full 32-byte absolute offsets for every jump. This approach is unnecessarily heavy. A more ergonomic and lightweight alternative would be to use relative jumps based on the current execution point (known as the program counter). In most cases, such as typical if or while logic, the jump distances are small and could easily be expressed in a single byte. The inefficiency of absolute jumps is well-known, and there is ongoing discussion in the Ethereum ecosystem about adopting relative jumps in future virtual machine designs.

What’s Happening?

  1. Input is loaded from calldata.

  2. Compared with 10 using the GT (greater than) opcode.

  3. If true, we JUMPI to a labeled part of the code where we return 0x01.

  4. If false, the EVM continues forward and returns 0x00 and jumps to the merge point

  5. The merge point is where both true and false branches reunite and returns the result

Both branches store the result at the stack, then on the merge point a 32-byte value padded with zeros is returned.

Try It Yourself

Modify it and check it using this EVM Disassembler to:

  • Return 0xAA if the input is less than 5

  • Return 0xBB if the input is between 5 and 20

  • Return 0xCC otherwise

So far, we've explored bytecode piece by piece, but what does a real smart contract look like in raw bytecode?

Next Tuesday, we’ll take a full contract and break it down section by section. Subscribe now and see you next Tuesday for more bytecode.


Control Flow in Bytecode