>/D_
Published on

Understanding Smart Contract Execution in Ethereum

Authors
  • avatar
    Name
    Frank
    Twitter

Understanding Smart Contract Execution in Ethereum

In this article, we'll explore how smart contracts are executed on the Ethereum network. We'll break down the role of the Ethereum Virtual Machine (EVM), how gas supply affects execution, and the recursive nature of contract calls. We'll also delve into how instructions are executed within the EVM.

Ethereum Virtual Machine (EVM) Instantiation

When executing smart contract code, the Ethereum Virtual Machine (EVM) is instantiated to process the instructions. The EVM setup involves several key steps:

  • Loading Contract Code: The code of the contract account is loaded into the program code ROM (Read-Only Memory).
  • Initializing the Program Counter: The program counter is set to zero, marking the starting point of execution.
  • Loading Contract Storage: The storage data from the contract account is loaded, which contains the contract's persistent state.
  • Memory Initialization: The EVM's memory is initialized to all zeros, providing a clean slate for execution.
  • Setting Environment Variables: Block and environment variables are set, such as the current block number, timestamp, and sender's address.

Gas Supply and Consumption

Gas is a fundamental aspect of Ethereum that measures the computational work required to execute operations.

  • Initial Gas Supply: The gas supply is set to the amount paid by the sender initiating the transaction.
  • Gas Consumption: As the code executes, gas is consumed based on the computational complexity of each instruction.
  • Out of Gas (OOG) Exception: If the execution runs out of gas, an OOG exception is triggered, immediately halting execution.

Execution and State Management

Think of the EVM as running on a sandboxed copy of the Ethereum world state:

  • Sandboxed Execution: If execution cannot complete (e.g., due to an OOG exception), the sandboxed version is discarded entirely.
  • Transaction Abandonment: If execution halts, the transaction is abandoned with no changes to the Ethereum state, except:
    • The sender's nonce is incremented.
    • The sender's Ether balance is reduced to pay for the resources used.
  • Successful Execution: If execution completes successfully, the real-world state is updated to match the sandboxed version, including:
    • Changes to contract storage.
    • Creation of new contracts.
    • Ether transfers.

The Recursive Nature of Contract Execution

Smart contracts can interact with other contracts, making code execution recursive.

  • Contract Calls: A contract can call other contracts, with each call instantiating a new execution context.
  • Sandboxed World State: Each new EVM instantiation has its sandboxed world state initialized from the level above.
  • Gas Supply for Nested Calls: Each call receives a specified amount of gas, not exceeding the remaining gas from the caller.
    • If the gas supply is insufficient, execution halts with an exception.
  • Handling Exceptions: If an exception occurs:
    • The sandboxed state is discarded.
    • Execution returns to the EVM at the level above, allowing exceptions to bubble up.

Clarifying Misconceptions About EVM Instances

It's important to clarify how the EVM handles contract interactions:

  • Single EVM Instance: When a transaction is executed, it's processed by a single EVM instance.
  • Execution Contexts: Each contract call creates a new execution context or environment within the same EVM.
    • This context includes its own memory, stack, and portion of available gas.
  • Nested Calls: Contract calls are nested, similar to function calls in traditional programming languages.
    • The called contract executes within the context of the original EVM process but has an isolated execution environment.

Analogy to Programming Concepts

Understanding EVM execution can be easier when compared to familiar programming concepts:

  • Function Calls: Contract calls are akin to function calls where each has its local scope and variables but runs in the same process.
  • Exception Handling: Exceptions in contract calls are handled similarly to exceptions in programming languages, allowing the calling contract to catch and manage them.
  • State Reversion: If a contract call fails, the state changes in that call are reverted, but previous state changes remain unless explicitly reverted.

Executing Instructions

Example using four instructions below:

PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE

  • PUSH1 0x60

    • PUSH1 is an EVM instruction that pushes the byte following the opcode onto the stack.
    • In this case, 0x60 (which is 96 in decimal) is pushed onto the stack.
  • PUSH1 0x40

    • Another PUSH1 opcode, this time pushing 0x40 onto the stack.
    • This action places 0x40 on top of the stack, with 0x60 beneath it.
  • MSTORE

    • MSTORE is a memory store operation in the EVM.
    • It takes two arguments from the stack: the first is the memory address, and the second is the value to be stored.
    • Here, 0x40 is used as the memory address (popped first), and 0x60 as the value to be stored (popped second).
    • After execution, 0x60 is stored at memory location 0x40, and the stack is emptied.
  • CALLVALUE

    • CALLVALUE is an environmental opcode.
    • It pushes the amount of ether (in wei) sent with the message call that initiated the execution onto the top of the stack.

The MSTORE opcode in the Ethereum Virtual Machine (EVM) is designed to interpret its arguments in a specific way:

  1. First Argument as Memory Address:

    • The first argument (0x40 in this case) is interpreted as the memory address. This is where the EVM will store the data.
    • The EVM understands that this value represents a location in its memory space.
  2. Second Argument as Data to Store:

    • The second argument (0x60) is the data that will be stored at the specified memory address.
    • In this context, 0x60 is treated as a raw byte of data. It's not interpreted as an integer or any other specific type at the time of storage. It's just binary data.
  3. Storing the Data:

    • The EVM takes the binary representation of 0x60 and stores it at the memory location 0x40.
    • Memory in the EVM is byte-addressable, meaning each address refers to a single byte. Since 0x60 is a single byte, it fits perfectly into the memory slot at 0x40.
  4. Retrieving and Interpreting the Data:

    • When this data is later retrieved, how it is interpreted depends on the context in which it is used.
    • If a subsequent operation treats the data at memory location 0x40 as an integer, then 0x60 will be interpreted as 96 in decimal.
    • The interpretation of the data depends on the instructions that are processing it.

Execution Process Summary

When a Solidity program is compiled into bytecode and run on the Ethereum Virtual Machine (EVM), the execution flow between functions involves several steps and EVM opcodes. Here's a high-level overview of how the EVM handles function calls and returns:

Function Call Mechanism

  1. Function Selector: Each function in a Solidity contract has a unique function selector, which is the first 4 bytes of the Keccak-256 hash of the function's signature (e.g., transfer(address,uint256)). This selector is used to identify the function being called.

  2. Dispatch Table: The compiled bytecode includes a dispatch table at the beginning, which maps function selectors to their corresponding bytecode offsets.

  3. CALLDATA: When a function is called, the caller provides the function selector and arguments in the calldata. The EVM reads the function selector from the calldata to determine which function to execute.

  4. JUMP and JUMPI Opcodes: The dispatch table uses JUMP and JUMPI opcodes to transfer control to the correct function based on the function selector.

    • The EVM extracts the function selector from the calldata.
    • It uses the dispatch table to find the byte offset for the corresponding function.
    • It then uses the JUMP opcode to transfer execution to that offset.

Function Execution

  1. Function Prologue: Once the EVM jumps to the function's bytecode, it executes the function prologue, which typically involves setting up the stack, memory, and local variables.

  2. Function Body: The actual logic of the function is executed. This includes handling arguments, performing computations, and interacting with storage or other contracts.

Internal Function Calls

  1. JUMPDEST: Solidity compiles internal function calls using JUMP and JUMPI opcodes. Each function has a JUMPDEST opcode at the start, marking the destination for jumps.

  2. Stack Management: Arguments are pushed onto the stack before making the internal call. The function being called pops these arguments from the stack.

  3. Return Values: After execution, the called function can push return values onto the stack. The calling function then pops these values for further processing.

External Function Calls

  1. CALL and DELEGATECALL Opcodes: For calling functions in other contracts, the EVM uses the CALL, DELEGATECALL, CALLCODE, and STATICCALL opcodes.

    • CALL is used for regular calls.
    • DELEGATECALL and CALLCODE are used for proxy patterns and delegate calls.
    • STATICCALL is used for view/pure function calls which do not modify state.
  2. Gas Management: The caller specifies the amount of gas for the call. If the called function runs out of gas, it will revert.

  3. Return Data: The called contract returns data to the caller, which can be retrieved using the RETURNDATASIZE and RETURNDATACOPY opcodes.

Example

Here is a simplified example of how the EVM handles a function call in bytecode:

pragma solidity ^0.8.0;

contract Example {
    function foo(uint256 x) public returns (uint256) {
        return bar(x + 1);
    }

    function bar(uint256 y) internal returns (uint256) {
        return y * 2;
    }
}

When compiled, the bytecode for foo and bar functions will include:

  • A dispatch table mapping the function selector of foo to its bytecode offset.
  • JUMP opcodes to transfer control to foo.
  • Within foo, a JUMP to bar with the argument x + 1 pushed onto the stack.
  • In bar, the result of y * 2 is pushed onto the stack and returned to foo.

Summary

In summary, the EVM uses a combination of opcodes, including JUMP and JUMPI, to transfer control between functions. Function selectors and dispatch tables are used to locate the correct function bytecode, and the stack is used to manage arguments and return values during function calls.

Understanding how the EVM executes smart contracts is crucial for developing efficient and secure Ethereum applications. By grasping these concepts, developers can write smarter contracts, optimize gas usage, and handle exceptions effectively.

My shorthand notes were the source material for this article produced by generative AI.