Yul Intermediate Language Documentation

repository·master·Indexed 42 days ago

https://github.com/andreitoma8/learn-yul

Educational notes and examples for Yul, an intermediate language for the Ethereum Virtual Machine (EVM). Covers syntax, variables, arithmetic, control flow, and environment functions. Includes detailed guides on managing EVM storage (sload, sstore), memory layout, and interacting with Solidity via inline assembly.

Tokens
8.8K
Snippets
27
Records
35
Agent score
4%

What's inside Yul

  1. What is Yul and how to use it in Solidity

    master

    Yul is an intermediate language used by the Solidity compiler. It can be used in stand-alone mode or as "inline assembly" within Solidity contracts. When using inline assembly, you wrap your Yul code in an assembly block. In this mode, variables are initialized on the stack.

    To use Yul inside a Solidity function, use the assembly keyword.

    contract C {
        function f() public {
            assembly {
                // Yul code goes here
            }
        }
    }
  2. How contracts are structured in Yul

    master

    In Yul, contracts are represented as objects. The code section contains the actual executable Yul code. You can use the object section to define nested objects, functions, and variables that are accessible within the code section. Note that Yul does not automatically respect call data or function selectors; you must manage these manually.

    To implement a contract that returns specific data, you can use nested objects to separate the deployment logic from the runtime logic.

    object "FullyYul" {
        code {
            // Deployment logic
            sstore(0, caller())
            datacopy(0x00, dataoffset("runtime"), datasize("runtime"))
            return(0x00, datasize("runtime"))
        }
    
        object "runtime" {
            code {
                // Runtime logic
                datacopy(0x00, dataoffset("Message"), datasize("Message"))
                return (0x00, datasize("Message"))
            }
    
            data "Message" "Hello World"
        }
    }
  3. Accessing Dynamic Arrays in memory

    master

    Dynamic arrays in Solidity memory have a specific layout:

    1. The first 32-byte slot stores the length of the array.
    2. The subsequent slots store the actual elements.

    In Yul, the variable representing a dynamic array is actually the location (pointer) of the first slot (the length slot).

    Warning: If a dynamic array is declared in Solidity but not initialized (e.g., bytes memory b;), its pointer defaults to the zero slot (0x60). You must manually initialize it in assembly to avoid memory collisions.

    function f(uint256[] memory arr) external {
        assembly {
            let location := arr
            let length := mload(arr) // The first slot is the length
            let firstElement := mload(add(arr, 0x20)) // Elements start after length
        }
    }
  4. Understanding Yul Memory

    master

    Memory is used for temporary storage of variables and is cleared at the end of a function call. It is used for:

    • Return values to external calls
    • Setting function arguments for external calls
    • Retrieving values from external calls
    • Reverting with error strings
    • Logging messages
    • Creating contracts
    • Using keccak256

    Memory is laid out in 32-byte sequences.

  5. Implement a Yul fallback function with a switch statement

    master

    To route calls to different logic within a Yul fallback function, use the switch statement to compare the function selector.

    1. Extract the selector from calldata using shr(0xe0, calldataload(0)) (which shifts right 224 bits to isolate the last 4 bytes).
    2. Use switch selector with case blocks for each function signature.
    3. Use default to handle unknown calls (e.g., by reverting).

    Note: switch in Yul only supports equality comparisons. Once a case matches, execution enters that block and does not check other cases.

    fallback(bytes calldata data) external returns (bytes memory returnData) {
        assembly{
            let callData := calldataload(0)
            let selector := shr(0xe0, callData)
    
            switch selector
            case 0x259c137d { 
                // logic for get23()
            }
            case 0x7cf5dab0 { 
                // logic for increment()
            }
            default { revert(0,0) }
        }
    }
  6. Accessing Structs and Fixed Arrays in memory

    master

    In memory, Solidity structs and fixed arrays are laid out sequentially as 32-byte slots.

    • Structs: Each field occupies one 32-byte slot.
    • Fixed Arrays: Each element occupies one 32-byte slot.

    To access them in Yul, you calculate the offset from the base memory address.

    struct S { uint256 a; uint256 b; }
    S memory s = S(a: 1, b: 2);
    
    assembly {
        // If s is at 0x80:
        let a := mload(0x80) // returns 1
        let b := mload(0xa0) // returns 2
    }
  7. Yul Types and Literals

    master

    Yul effectively has only one type: bytes32. This type can hold any value, and the compiler automatically inserts necessary conversions.

    Supported literals include:

    • Integer constants: Decimal or hexadecimal notation.
    • ASCII strings: e.g., "abc", supporting hex escapes \xNN and Unicode escapes \uNNNN.
    • Hex strings: e.g., hex"616263".
    function f() public pure returns (bool, uint256, bytes32) {
            bool x;
            uint256 y;
            bytes32 z;
    
            assembly {
                x := 1
                y := 0xa
                z := "Hello World!"
            }
    
            return( x, y, z);
        }
  8. Manual EVM Calls and Function Selectors

    master

    Yul does not have high-level abstractions like interfaces, abi.encodeWithSignature, or function selectors. To call a function, you must manually construct the payload:

    1. Function Selector: The first 4 bytes of the keccak256 hash of the function signature (e.g., keccak256("balanceOf(address)")[0:4]).
    2. Arguments: The abi.encoded arguments appended after the selector.

    EVM Transaction Components:

    • origin(): The sender of the transaction (tx.origin).
    • gasprice(): The gas price.
    • caller(): The current msg.sender.
    • call(addr, value, in, in_size, out, out_size): Used to execute calls.
  9. How Solidity manages memory layout

    master

    When writing Yul/assembly within Solidity, you must respect the memory layout Solidity reserves:

    • [0x00 - 0x20]: Scratch space (first 2x32 bytes).
    • [0x20 - 0x40]: Free memory pointer (the location of the next free memory slot).
    • [0x40 - 0x60]: Zero slot (reserved for initial values of dynamic memory arrays; never write to this).
    • [0x60 - 0x80]: Reserved/Empty.
    • [0x80 - ...]: The start of general memory usage.

    To find the next available memory slot, load the value at 0x40.

    assembly {
        let freeMemoryPointer := mload(0x40)
    }
  10. Control Flow: For Loops, If Statements, and Switch

    master

    For Loops

    You can use a for loop with two syntaxes: declaring the iterator inside the loop header or declaring it before the loop.

    for {let i := 0} lt(i, n) {i := add(i, 1)} { /* code */ }

    Use the break statement to exit a loop.

    If Statements

    Yul has no boolean type. Any value other than 0 is considered true. Use iszero(x) to check for equality to zero.

    Switch Statements

    Switch statements allow checking multiple cases. Only one case can be true; once a match is found, subsequent cases are not checked. A default case handles unmatched values.

    // If Statement Example
    if 2 { // 2 is true }
    if 0 { // 0 is false }
    
    // Switch Statement Example
    switch n
    case 0 { /* n == 0 */ }
    case 1 { /* n == 1 */ }
    default { /* neither */ }
  11. Accessing Fixed Arrays in Storage

    master

    For a fixed-size array, the location of an element is array.slot + index.

    Standard Fixed Array:

    uint256[5] arr;
    // Access index: sload(add(arr.slot, index))

    Packed Fixed Array (elements < 32 bytes): If elements are smaller than 32 bytes, they are packed. To retrieve one, load the slot, shift it right by the byte offset, and mask it.

    uint256[5] arr;
    
    function get(uint256 index) public view returns (uint256 value) {
        assembly {
            value := sload(add(arr.slot, index))
        }
    }
  12. Implement safe arithmetic to prevent overflow

    master

    Yul has no built-in overflow protection. You must manually implement checks for addition, subtraction, and multiplication to ensure safety.

    Addition

    Check if the sum of two values is less than either of the original values.

    Subtraction

    Check if the minuend is less than the subtrahend before performing the operation.

    Multiplication

    Check if the product divided by the first value equals the second value. You must also handle the edge case where the first value is zero.

    // Safe Addition
    function safeAdd(a, b) -> result {
        r := add(a, b)
        if or(lt(r, a), lt(r, b)) {
            revert(0,0)
        }
    }
    
    // Safe Subtraction
    function safeSub(a, b) -> result {
        if lt(a, b) {
            revert(0,0)
        }
        result := sub(a, b)
    }
    
    // Safe Multiplication
    function safeMul(a, b) -> result {
        switch a
        case 0 {result := 0}
        default {
            result := mul(a, b)
            if iszero(eq(div(result, a), b)) {
                revert(0,0)
            }
        }
    }