- Published on
Understanding Smart Contract Execution in Ethereum
- Authors
- Name
- Frank
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 is96
in decimal) is pushed onto the stack.
PUSH1 0x40
- Another
PUSH1
opcode, this time pushing0x40
onto the stack. - This action places
0x40
on top of the stack, with0x60
beneath it.
- Another
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), and0x60
as the value to be stored (popped second). - After execution,
0x60
is stored at memory location0x40
, 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:
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.
- The first argument (
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.
- The second argument (
Storing the Data:
- The EVM takes the binary representation of
0x60
and stores it at the memory location0x40
. - 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 at0x40
.
- The EVM takes the binary representation of
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, then0x60
will be interpreted as96
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
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.Dispatch Table: The compiled bytecode includes a dispatch table at the beginning, which maps function selectors to their corresponding bytecode offsets.
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.
JUMP and JUMPI Opcodes: The dispatch table uses
JUMP
andJUMPI
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
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.
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
JUMPDEST: Solidity compiles internal function calls using
JUMP
andJUMPI
opcodes. Each function has aJUMPDEST
opcode at the start, marking the destination for jumps.Stack Management: Arguments are pushed onto the stack before making the internal call. The function being called pops these arguments from the stack.
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
CALL and DELEGATECALL Opcodes: For calling functions in other contracts, the EVM uses the
CALL
,DELEGATECALL
,CALLCODE
, andSTATICCALL
opcodes.CALL
is used for regular calls.DELEGATECALL
andCALLCODE
are used for proxy patterns and delegate calls.STATICCALL
is used for view/pure function calls which do not modify state.
Gas Management: The caller specifies the amount of gas for the call. If the called function runs out of gas, it will revert.
Return Data: The called contract returns data to the caller, which can be retrieved using the
RETURNDATASIZE
andRETURNDATACOPY
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 tofoo
.- Within
foo
, aJUMP
tobar
with the argumentx + 1
pushed onto the stack. - In
bar
, the result ofy * 2
is pushed onto the stack and returned tofoo
.
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.