Smart Contract Introduction

Introduction to the Ethereum Virtual Machine (EVM) and Solidity.

Smart Contract Fundamentals

Smart contracts are programs that reside and run on a blockchain. Once deployed on-chain, these contracts are programmed to automatically execute and enforce agreed-upon actions the moment predefined conditions are met.

The blockchain platform where the smart contract lives provides the necessary infrastructure for its execution. On Ethereum (and compatible chains like Polygon and Avalanche), this underlying engine is known as the Ethereum Virtual Machine (EVM).

A smart contract’s lifecycle consists of three core phases:

  1. Creation: Writing the logic and compiling the code.
  2. Deployment: Pushing the compiled bytecode to the blockchain.
  3. Execution: Interacting with the contract via transactions and triggering its functions.

Solidity

Solidity is the primary language used to write EVM-compatible smart contracts.

Core Language Features:

  • Contract-oriented: Contracts are the fundamental building blocks, functioning similarly to classes in object-oriented programming. They encapsulate state (data) and behavior (functions).
  • Data Types: Solidity supports standard value types (like uint, bool, and address) as well as complex reference types, including structs, mappings (hash tables), and both statically and dynamically sized arrays.
  • Events: Contracts can emit events using the LOG opcodes. These events are stored in the transaction’s receipt (not in contract storage, making them cheaper) and allow off-chain applications (like a web frontend) to listen for specific on-chain activities.
  • Inheritance and Modularity: Contracts can inherit from multiple other contracts. This allows developers to use modular, pre-audited code libraries (such as OpenZeppelin’s token standards or access controls) rather than writing everything from scratch.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0; // <- Solidity Version 

contract SimpleStorage { // <- Contract 
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

EVM Execution Mechanism:

  • The EVM does not execute Solidity directly. The Solidity compiler translates source code into EVM bytecode, just as a C or C++ compiler translates source code into machine code.
  • The EVM is Turing-complete, so in general it is impossible to know ahead of time whether a program will run forever. To prevent infinite loops from freezing the network, Ethereum uses gas. Every opcode has a gas cost, and if execution runs out of gas, the call reverts while the spent gas is still consumed.
  • The stack limit: The EVM is a stack-based virtual machine with a maximum stack depth of 1024 items. Many instructions can directly access only the top portion of the stack, which is one reason large functions can trigger the well-known “stack too deep” compiler error.
  • Memory vs. storage: During execution, memory is a temporary linear byte array that is cleared after the call frame ends. Memory expansion is initially cheap, but its cost increases as more memory is allocated in a single execution.

The EVM Architecture:

Solidity is the language we use to write many EVM-compatible smart contracts, while the Ethereum Virtual Machine is the runtime environment that executes the compiled bytecode.

EVM architecture

In the EVM, data does not live in one single place. Instead, different parts of a contract execution use different data locations, each with its own lifetime, gas cost, and purpose. Understanding the difference between memory, storage, calldata, stack, and transient storage is fundamental for writing efficient and secure smart contracts.

Memory:

Memory is a temporary, read-write byte array that exists only while a function call is executing. It is wiped completely after the current call frame ends, so it does not persist on-chain.

  • Used for temporary variables, ABI-decoded arguments, return data, and intermediate computations.
  • Cheap to access compared with storage, but expanding memory costs additional gas.
  • A value in memory disappears after the call finishes.

Storage:

Storage is the contract’s persistent state. This is the data that is actually kept on-chain and remains available across transactions.

  • Backed by the Ethereum state trie and addressed in 32-byte slots.
  • Reading uses SLOAD, and writing uses SSTORE.
  • Much more expensive than memory or calldata because every persistent change affects blockchain state.
  • Used for state variables such as balances, owners, mappings, arrays, and configuration values.
  • If you write to storage, the result is still there in the next transaction.

Stack:

The stack is the EVM’s small, ultra-fast working area for computation.

  • It stores 256-bit words.
  • Maximum depth is 1024 items.
  • Most EVM opcodes push values onto the stack and pop values from it.
  • It is extremely fast, but very limited in size and accessibility.
  • Solidity developers usually interact with it indirectly through expressions, local variables, and function execution.
  • The well-known “stack too deep” issue comes from this limitation.

Transient Storage (EIP-1153 - The Modern Addition):

Transient storage is a temporary key-value store that behaves like storage in addressing, but is automatically cleared at the end of the transaction.

  • Accessed with TSTORE and TLOAD.
  • Unlike memory, it is not just local to one call frame; it can be reused across multiple calls involving the same contract within the same transaction.
  • Unlike storage, nothing remains after the transaction ends.
  • Useful for patterns like reentrancy guards, temporary locks, and transaction-scoped state.
  • It avoids some of the gas/refund complexity of using persistent storage for temporary values.

Key intuition:

  • Storage = permanent on-chain state.
  • Memory = temporary workspace for one call.
  • Calldata = read-only transaction input.
  • Stack = tiny execution scratchpad for opcodes.
  • Transient storage = transaction-wide temporary state.

Comparison table:

Location Persistence Mutable? Scope Gas profile Typical use
Stack Only during current execution step / call Yes, through push/pop operations Current EVM execution context Fastest and cheapest Opcode operands, local intermediate values
Memory Cleared after the current call frame ends Yes Current call frame Cheap, but expansion increases cost Temporary arrays, strings, ABI decoding, return data
Storage Persists across transactions Yes Contract state Most expensive, especially writes State variables, mappings, balances, configuration
Transient storage Cleared at the end of the transaction Yes Transaction-scoped contract state Cheaper than persistent storage for temporary state Reentrancy locks, temporary flags, transaction-scoped coordination

Details:

Layout of Stack:

EVM stack layout

Layout in Memory:

Solidity reserves four 32-byte slots in memory:

  • 0x00 - 0x3f (64 bytes): scratch space for hashing methods. 0x00 → used as initial value for dynamic memory arrays and should never be written to.
  • 0x40 - 0x5f (32 bytes): currently allocated memory size (aka. free memory pointer)
  • 0x60 - 0x7f (32 bytes): zero slot
  • After that, the free memory pointer starts at 0x80.

Solidity always places new objects at the free memory pointer.

Elements in Solidity memory arrays always occupy multiples of 32 bytes. This is true even for bytes1[], but not for bytes and string. Multi-dimensional memory arrays are pointers to memory arrays.

EVM memory layout

Layout of State Variables in Storage and Transient Storage:

Storage and transient storage layouts are completely independent and do not interfere with each other’s variable locations. This means storage and transient storage variables can be interleaved safely. Only value types are supported for transient storage.

State variables are stored in storage in a compact way, so multiple values can sometimes share the same 32-byte slot. Except for dynamically sized arrays and mappings, data is stored contiguously starting with the first state variable in slot 0.

EVM storage layout

For each variable, a size in bytes is determined according to its type. Multiple, contiguous items that need less than 32 bytes are packed into a single storage slot if possible, according to the following rules:

  • The first item in a storage slot is stored lower-order aligned.
  • Value types use only as many bytes as are necessary to store them.
  • If a value type does not fit the remaining part of a storage slot, it is stored in the next storage slot.
  • Structs and array data always start a new slot and their items are packed tightly according to these rules.
  • Items following struct or array data always start a new storage slot.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.29;

struct S {
    int32 x;
    bool y;
}

contract A {
    uint a;
    uint128 transient b;
    uint constant c = 10;
    uint immutable d = 12;
}

contract B {
    uint8[] e;
    mapping(uint => S) f;
    uint16 g;
    uint16 h;
    bytes16 transient i;
    S s;
    int8 k;
}

contract C is A, B layout at 42 {
    bytes21 l;
    uint8[10] m;
    bytes5[8] n;
    bytes5 o;
}

The storage layout for contract A is shown below. The transient, constant, and immutable variables do not occupy regular persistent storage slots in this layout.

00 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

The storage layout for contract B is shown below. e and f are dynamic types, so their declared slots act as anchors for separately computed data locations. S is a struct containing x (4 bytes) and y (1 byte), which is why its members can be packed.

00 [eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee]
01 [ffffffffffffffffffffffffffffffff]
02 [                            hhgg]
03 [                           yxxxx]
04 [                               k]

The storage layout for contract C will be:

42 [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]
43 [eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee]
44 [ffffffffffffffffffffffffffffffff]
45 [                            hhgg]
46 [                           yxxxx]
47 [          lllllllllllllllllllllk]
48 [                      mmmmmmmmmm]
49 [  nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn]
50 [                      nnnnnnnnnn]
51 [                           ooooo]

How storage handles mappings and dynamic arrays: for a dynamic array stored at slot p, the slot itself stores the array length and the array data starts at keccak256(p). For mappings, the declared slot acts as a seed, but the values are stored at locations derived from keccak256(h(k) . p), where k is the key and h depends on the key type. Dynamic arrays of elements smaller than 16 bytes can still pack multiple elements into a single slot.

Dynamic arrays of dynamic arrays apply this rule recursively. For example, if x has type uint24[][] and is stored at slot p, the location of x[i][j] is computed as follows:

keccak256(keccak256(p) + i) + floor(j / floor(256 / 24))

Layout of Calldata:

Calldata is the read-only input area attached to an external function call.

  • It contains the function selector and ABI-encoded arguments.
  • It cannot be modified by the contract.
  • It is cheaper than copying the same data into memory because it already exists as transaction input.
  • Best used for external function parameters when the data only needs to be read.

See the Solidity ABI specification for the formal structure of calldata.

The calldata layout has two main parts:

The function selector (4 bytes):

  • It is the first (left, high-order in big-endian) four bytes of the Keccak-256 hash of the signature of the function.
  • Function signature: the canonical expression of the function prototype without data location specifiers

Argument Encoding:

  • Some common ABI types include:
  • uint<M>: unsigned integer type of M bits, 0 < M <= 256M % 8 == 0. e.g. uint32uint8uint256.
  • int<M>: two’s complement signed integer type of M bits, 0 < M <= 256M % 8 == 0.
  • address: equivalent to uint160, except for the assumed interpretation and language typing. For computing the function selector, address is used.
  • uintint: synonyms for uint256int256 respectively. For computing the function selector, uint256 and int256 have to be used.
  • bool: equivalent to uint8 restricted to the values 0 and 1. For computing the function selector, bool is used.
  • fixed<M>x<N>: signed fixed-point decimal number of M bits, 8 <= M <= 256M % 8 == 0, and 0 < N <= 80, which denotes the value v as v / (10 * N).
  • ufixed<M>x<N>: unsigned variant of fixed<M>x<N>.
  • fixedufixed: synonyms for fixed128x18ufixed128x18 respectively. For computing the function selector, fixed128x18 and ufixed128x18 have to be used.
  • bytes<M>: binary type of M bytes, 0 < M <= 32.
  • function: an address (20 bytes) followed by a function selector (4 bytes). Encoded identical to bytes24.
  • For the full list and the exact encoding rules, see the Solidity ABI specification.

There are two broad encoding categories:

  • Dynamic encoding: bytes, string, T[] for any T, T[k] for any dynamic T, and tuples (T1,...,Tk) where at least one component is dynamic
  • Static encoding: all other ABI types

More detail is available in the formal specification of the encoding.

Example:

If we want to call function bar(bytes3[2] memory) public pure {} with the two arguments ["abc", "def"]:

  • Function selector: the first four bytes of keccak256("bar(bytes3[2])"), which gives 0xfce353f6
  • Argument:
    • First parameter: "abc"0x61626300000000000000000000...000000000000000000000000000000
    • Second parameter: "def"0x6465660000000000000000000...0000000000000000000000000000000

Full calldata:

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

Fixed-size byte arrays such as bytes3 are left-aligned in their 32-byte slot and padded with zeros on the right. By contrast, integers and booleans are right-aligned and therefore padded with zeros on the left.

If we want to call function sam(bytes memory, bool, uint[] memory) public pure {} with the arguments "dave", true, and [1,2,3]:

  • Function selector: 0xa5643bf2, derived from sam(bytes,bool,uint256[])
  • 0x0000000000000000000000000000000000000000000000000000000000000060: offset to the first argument’s data, measured from the start of the arguments block. It is 0x60 because the head contains three 32-byte slots.
  • 0x0000000000000000000000000000000000000000000000000000000000000001: the encoded value of true
  • 0x00000000000000000000000000000000000000000000000000000000000000a0: offset to the third argument’s data. The first dynamic argument ("dave") occupies two 32-byte slots in the tail: one for the length and one for the data.
  • 0x0000000000000000000000000000000000000000000000000000000000000004: the length of the byte string "dave"
  • 0x6461766500000000000000000000000000000000000000000000000000000000: the UTF-8 bytes of "dave", padded with zeros on the right
  • 0x0000000000000000000000000000000000000000000000000000000000000003: the length of the array [1,2,3]
  • 0x0000000000000000000000000000000000000000000000000000000000000001: the first element
  • 0x0000000000000000000000000000000000000000000000000000000000000002: the second element
  • 0x0000000000000000000000000000000000000000000000000000000000000003: the third element

From code to bytecode:

If we compile a simple Solidity contract like this with Foundry (forge) using Solidity 0.8.27:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
    uint256 public number;

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }
}

Compiling this contract with Foundry (forge) using Solidity 0.8.27 produces bytecode like the following. If you want to inspect individual opcodes, evm.codes.

0x6080604052348015600e575f5ffd5b506101e18061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c80633fb5c1cb146100435780638381f58a1461005f578063d09de08a1461007d575b5f5ffd5b61005d600480360381019061005891906100e4565b610087565b005b610067610090565b604051610074919061011e565b60405180910390f35b610085610095565b005b805f8190555050565b5f5481565b5f5f8154809291906100a690610164565b9190505550565b5f5ffd5b5f819050919050565b6100c3816100b1565b81146100cd575f5ffd5b50565b5f813590506100de816100ba565b92915050565b5f602082840312156100f9576100f86100ad565b5b5f610106848285016100d0565b91505092915050565b610118816100b1565b82525050565b5f6020820190506101315f83018461010f565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61016e826100b1565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101a05761019f610137565b5b60018201905091905056fea264697066735822122009d4f747f065e2de2101ad2f0bfe6fc884a4af0ad4801bdf97b4033939038b6264736f6c634300081b0033

We can divide the bytecode like this:

Creation code:

6080604052348015600e575f5ffd5b506101e18061001c5f395ff3fe

Runtime Initialization & Fallback Check

608060405234801561000f575f5ffd5b50

The Function Dispatcher:

6004361061003f575f3560e01c80633fb5c1cb146100435780638381f58a1461005f578063d09de08a1461007d575b5f5ffd

Function Code:

5b61005d600480360381019061005891906100e4565b610087565b005b610067610090565b604051610074919061011e565b60405180910390f35b610085610095565b005b805f8190555050565b5f5481565b5f5f8154809291906100a690610164565b9190505550565b5f5ffd5b5f819050919050565b6100c3816100b1565b81146100cd575f5ffd5b50565b5f813590506100de816100ba565b92915050565b5f602082840312156100f9576100f86100ad565b5b5f610106848285016100d0565b91505092915050565b610118816100b1565b82525050565b5f6020820190506101315f83018461010f565b92915050565b

Panic Handler:

7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61016e826100b1565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101a05761019f610137565b5b60018201905091905056

The Metadata Hash:

1
fea264697066735822122009d4f747f065e2de2101ad2f0bfe6fc884a4af0ad4801bdf97b4033939038b6264736f6c634300081b0033

Creation code:

Creation code runs only once, during deployment. Its job is to perform any deployment-time checks, copy the runtime bytecode into memory, and return that runtime bytecode so it becomes the contract code stored on-chain.

Contract creation code flow

→ Running all the bytecode will return the runtime code (on chain) → all the after just return the deploy part.

Deployed bytecode is used because the EVM needs to know what code to actually save to the blockchain. The goal of deploy bytecode is to copy runtime bytecode into the EVM’s temporary memory, and then use the RETURN opcode to save to the blockchain (deploy).

---------- Set up free pointer ----------
60 80     = PUSH1 0x80   // Push 0x80
60 40     = PUSH1 0x40   // Push 0x40
52        = MSTORE       // Setup free memory pointer
---------- Check non-payable contract -----------
34        = CALLVALUE    // Get msg.value (ETH sent)
80        = DUP1         // Duplicate value
15        = ISZERO       // Check if msg.value is 0
60 0e     = PUSH1 0x0e   // Push valid jump address (0x0e)
57        = JUMPI        // Jump if no ETH was sent
5f        = PUSH0        // Push 0
5f        = PUSH0        // Push 0
fd        = REVERT       // Revert if ETH was sent (non-payable)
---------- Copy the runtime code and save it on chain ---------
5b        = JUMPDEST     // Safe landing pad (0x0e)
50        = POP          // Clean up stack
61 01e1   = PUSH2 0x01e1 // Runtime code size (481 bytes)
80        = DUP1         // Duplicate size
61 001c   = PUSH2 0x001c // Runtime code starting position
5f        = PUSH0        // Memory destination (0)
39        = CODECOPY     // Copy runtime code into memory
5f        = PUSH0        // Memory start (0)
f3        = RETURN       // Save runtime code to the blockchain
-------- REVERT -----------------
fe        = INVALID      // End of deployment instructions

Runtime Initialization & Fallback Check:

This part runs every time when a user is interact with the contract. It essentially rechecks non-payable because none of our function receive ETH it will revert if anyone sends ETH to the contract.

-------------- Set up free pointer ---------------
60 80       =   PUSH1 0x80     // Push 0x80 (128)
60 40       =   PUSH1 0x40     // Push 0x40 (64)
52          =   MSTORE         // Setup free memory pointer (reserves memory space)
-------------- Recheck non-payable - like the part on deploy code----
34          =   CALLVALUE      // Get msg.value (amount of ETH sent in this transaction)
80          =   DUP1           // Duplicate the ETH value on the stack
15          =   ISZERO         // Check msg.value == 0 ? 1: 0 
61 000f     =   PUSH2 0x000f   // Push the safe jump destination (0x000f)
57          =   JUMPI          // Jump to 0x000f if NO ETH was sent (ISZERO was 1)
5f          =   PUSH0          // Push 0 
5f          =   PUSH0          // Push 0 
fd          =   REVERT         // REVERT the transaction if ETH *was* sent
------------- Continue ----------------

5b          =   JUMPDEST       // Safe landing pad (Position 0x000f)
50          =   POP            // Clean up the leftover CALLVALUE from the stack

The Function Dispatcher:

When we compile a contract it will have a function dispatcher part. It can be easily seen as a giant switch case that match the call data with correct function selector:

  • Function selector are defined as the first four bytes of the Keccak hash of the canonical representation of the function signature.

The canonical representation of the function signature is the function name along with the function argument types.

Function: function setNumber(uint256 newNumber) public 
Canonical representation / Function Signature : setNumber(uint256)
Function Selector: 0x3fb5c1cb

Explanation of the dispatcher:

----------- Check whether CALLDATA is at least 4 bytes long: the dispatcher needs a 4-byte function selector. --------
60 04                       =   PUSH1 0x04       // Push 4 (bytes) to stack.
36                          =   CALLDATASIZE     // Get size of transaction data.
10                          =   LT               // Check calldata < 4 ? 1: 0
61 003f                     =   PUSH2 0x003f     // Push fallback jump address.
57                          =   JUMPI            // Jump to fallback if size < 4 (no function called).
--------- Load the transaction data - get the 4 bytes function selector -------
5f                          =   PUSH0            // Push 0 to stack.
35                          =   CALLDATALOAD     // Load first 32 bytes of calldata.
60 e0                       =   PUSH1 0xe0       // Push 224 (256 bits - 32 bits).
1c                          =   SHR              // Extract the 4-byte function selector
--------- Switch case table ----------------
---- Case: setNumber(uint256) ----- 
80                          =   DUP1             // Duplicate the isolated selector.
63 3fb5c1cb                 =   PUSH4 0x3fb5c1cb // Push selector `setNumber(uint256)`.
14                          =   EQ               // Check equal function signature
61 0043                     =   PUSH2 0x0043     // Push `setNumber` function address.
57                          =   JUMPI            // Jump to `setNumber` if matched.
---- Case: number() ------
80                          =   DUP1             // Duplicate selector again.
63 8381f58a                 =   PUSH4 0x8381f58a // Push selector for `number()`.
14                          =   EQ               // Does calldata selector match `number()`?
61 005f                     =   PUSH2 0x005f     // Push `number()` function address.
57                          =   JUMPI            // Jump to `number()` if matched.
---- Case: increment() ----
80                          =   DUP1             // Duplicate selector again.
63 d09de08a                 =   PUSH4 0xd09de08a // Push selector for `increment()`.
14                          =   EQ               // Does calldata selector match `increment()`?
61 007d                     =   PUSH2 0x007d     // Push `increment()` function address
57                          =   JUMPI            // Jump to `increment()` if matched.
5b                          =   JUMPDEST         // Fallback destination (if no functions matched).
----- Default case: no selector matched --------
5f                          =   PUSH0            // Push 0.
5f                          =   PUSH0            // Push 0.
fd                          =   REVERT           // Revert (function not found).

Function Code:

This section has three parts:

  • Wrappers: manage I/O and unpack calldata
  • Core logic: execute the function logic
  • ABI helpers: reusable utility code to decode variables

Part 1: Wrappers

  • The wrapper for setNumber(uint256) unpacks the uint256 argument and then jumps to the core logic.
5b61005d600480360381019061005891906100e4565b610087565b00

Explanation:

Important note: all offsets are counted from byte 0x00 of the runtime bytecode.

----------- Entry Point & Set Final Return Address --------
5b      =   JUMPDEST         // Entry point for setNumber(uint256)
61 005d =   PUSH2 0x005d     // Push return address 
--------- Calculate Arguments Size (Total - 4 bytes) --------
60 04   =   PUSH1 0x04       // Push offset for the function selector (4 bytes)
80      =   DUP1             // Duplicate the 4
36      =   CALLDATASIZE     // Get the total size of the incoming transaction data
03      =   SUB              // Subtract 4 from total size -> get correct size of args
--------- Setup Stack for ABI Decoder (Start, End, Return) --------
81      =   DUP2             // Duplicate the 4 again (Start Index)
01      =   ADD              // Add 32 and 4 to get End Index (36)
90      =   SWAP1            // Swap to organize stack
61 0058 =   PUSH2 0x0058     // Push the intermediate return address
91      =   SWAP2            // Rearrange
90      =   SWAP1            // Stack is now: [Start(4), End(36), Return(0x58)]
--------- Jump to ABI Decoder Helper --------
61 00e4 =   PUSH2 0x00e4     // Push the address for the ABI Decoding Helper
56      =   JUMP             // DECODE: Jump to Helper to extract uint256
--------- Return from Decoder & Jump to Core Logic --------
5b      =   JUMPDEST         // Landing pad (0x0058) from Decoder
61 0087 =   PUSH2 0x0087     // Push the address for the Core Logic
56      =   JUMP             // Jump to Core Logic 
--------- Final Return & Stop --------
5b      =   JUMPDEST         // Landing pad (0x005d) from Core Logic
00      =   STOP             // Transaction complete!

The other wrappers follow the same overall pattern.

Part 2: Core logic:

The core logic for setNumber(uint256) is:

5b805f819055505056
----------- Core Logic: setNumber(uint256) --------
5b  =   JUMPDEST         // Entry point from the Wrapper (Position 0x0087)
80  =   DUP1             // Duplicate 'newNumber' 
5f  =   PUSH0            // Push 0 - 'number' lives in Storage Slot 0
81  =   DUP2             // Duplicate the slot number 0
90  =   SWAP1            // Arrange the stack for SSTORE: [Slot (0), Value (newNumber)]
55  =   SSTORE           // WRITE: Save 'newNumber' into Storage Slot 0
50  =   POP              // Clean up 
50  =   POP              // Clean up the stack 
56  =   JUMP             // Jump back to the Wrapper 

Part 3: ABI helper at position 0x00e4

----------- Check if CALLDATA args size >= 32 bytes --------
5b                          =   JUMPDEST         // Entry point from the Wrapper 
5f                          =   PUSH0            // Push 0
60 20                       =   PUSH1 0x20       // Push 32 (Expected size of a uint256)
82                          =   DUP3             // Duplicate available calldata size
84                          =   DUP5             // Duplicate expected size (32)
03                          =   SUB              // Subtract to calculate difference
12                          =   LT               // Check if available < 32 ? 1 : 0
15                          =   ISZERO           // Invert answer 
61 00f9                     =   PUSH2 0x00f9     // Push success jump address
57                          =   JUMPI            // Jump to slicer if length is correct
--------- Error Case: Revert if too short --------
61 00f8                     =   PUSH2 0x00f8     // Push dummy return address
61 00ad                     =   PUSH2 0x00ad     // Push Error/Revert block address
56                          =   JUMP             // Crash transaction (Not enough data sent)
--------- Slicer: Extract the uint256 --------
5b                          =   JUMPDEST         // Entry point for Slicer (0x00f9)
5f                          =   PUSH0            // Push 0
81                          =   DUP2             // Duplicate data offset (4)
35                          =   CALLDATALOAD     // SLICE: Read 32 bytes starting exactly at byte 4
90                          =   SWAP1            // Move the clean uint256 to the top of the stack
50                          =   POP              // Clean up the '4' offset

Panic Handler

This chunk of bytecode is the global panic handler. If a function such as increment() detects an overflow, it jumps here to build the error payload and revert. In this case the payload encodes Panic(0x11), which is Solidity’s panic code for arithmetic overflow or underflow.

--------- Push the Panic Selector to Memory ---------
7f 4e487b71000...00 = PUSH32 0x4e487b71... // Push the 4-byte selector for Panic(uint256), padded with zeros to 32 bytes
5f                  = PUSH0                // 
52                  = MSTORE               // Store the Panic selector in memory at 0x00

--------- Push the Error Code (0x11) to Memory ---------
60 11               = PUSH1 0x11           // Push 17 - 0x11 = standard code for overflow
60 04               = PUSH1 0x04           // Push 4 
52                  = MSTORE               // Store the 0x11 error code into memory

--------- Revert the Transaction ---------
60 24               = PUSH1 0x24           // Push 36 4 bytes selector + 32 bytes uint256 = 36 bytes
5f                  = PUSH0                // Push 0 memory offset for the return data
fd                  = REVERT               // REVERT

The Metadata Hash

fe = INVALID (The EVM execution barrier)

--- Start CBOR Dictionary ---
a2 = Map(2) -> Tells the decoder "This is a dictionary with 2 key-value pairs"

--- Key/Value Pair 1: IPFS Hash ---
64 = String(4) -> next 4 bytes are a string
  69 70 66 73 = "ipfs" (The Key)
58 22 = Bytes(34) -> next 34 (0x22) bytes are a byte array
  12 20 = Multihash prefix (12 = SHA2-256, 20 = 32 bytes length)
  09d4f747f065e2de2101ad2f0bfe6fc884a4af0ad4801bdf97b4033939038b62 = The actual 32-byte IPFS Hash (The Value)

--- Key/Value Pair 2: Compiler Version ---
64 = String(4) -> next 4 bytes are a string
  73 6f 6c 63 = "solc" (The Key)
43 = Bytes(3) -> next 3 bytes are a byte array
  00 08 1b = The compiler version (0.8.27, since 0x1b is 27 in decimal)

--- Metadata Length Marker ---
00 33 = Metadata length (51 bytes)

References

  1. Solidity Layout in Memory

    • Official description of Solidity’s reserved memory regions and memory allocation rules.
  2. Solidity Layout of State Variables in Storage and Transient Storage

    • Official description of storage packing, mappings, dynamic arrays, and transient storage.
  3. Solidity ABI Specification

    • Official Solidity ABI specification.
  4. Formal Specification of the Encoding

    • Formal encoding rules for Solidity ABI types.
  5. Contract Metadata

    • Official explanation of the CBOR metadata appended to Solidity bytecode.
  6. EIP-1153: Transient Storage Opcodes

    • Specification for TLOAD and TSTORE.
  7. EVM Codes

    • EVM opcode reference and interactive explorer.
updatedupdated2026-03-252026-03-25