Day 1: EVM Architecture Fundamentals
EVM Architecture, Gas Model and Opcodes
- EVM Stack Machine
- Critical Opcodes
- Assemby in Solidity
- Gas Mechanics(EIP-1559)
EVM Stack Machine
The EVM is a stack-based virtual machine that executes smart contracts on the Ethereum blockchain. It is a 256-bit word stack machine with the following constraints:
- Stack Depth: Maximum of 1024 items.
- Word Size: Each item is a 256-bit word.
- Memory: Byte-addressable, expandable, and volatile during execution.
- Stack Operations: LIFO(Last In, First Out)
It is a virtual machine that runs on each Ethrereum node, executing contract bytecode and is in charge of keeping the blockchain alive. The EVM has its own set of instructions called opcodes. Opcodes are low-level instructions that executes specific operations like reading/writing to storage, calling other contracts, performing arithmetic operations, etc. The stack is used to hold intermediate values during execution. When a program runs, it pushes values onto the stack, performs operations using opcodes, and pops results off the stack. The EVM also has a memory area that is used for temporary storage during execution.
Why is the stack size just 1024?
1024 is a fair limit that balances performance and resource constraints. A larger stack would require more memory and processing power. Having a dynamic stack size complicates the implementation and can lead to inefficiencies.
Opcodes
What does the Ethereum actually do when you send a transaction? This is where the opcodes come in. Opcodes are the low-level instructions that the EVM understands and executes. You can find the complete list of opcodes in the EVM Opcodes Interactive Reference. Think of opcodes as the assembly language for the EVM. Each opcode corresponds to a specific operation that the EVM can perform. Let’s look at an example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleAdder {
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
}
We have a basic function that adds two numbers. When we compile this contract, the Solidity compiler translates it into EVM bytecode, which consists of a series of opcodes. If you compile the above contract using the Solidity compiler with optimization enabled, you can see the generated bytecode and opcodes. You can use the following command:
solc --bin --optimize --asm SimpleAdder.sol
Let’s examine the opcodes generated:
...
PUSH1 0x00 // memory slot for return
CALLDATALOAD // load first argument (a)
PUSH1 0x20
CALLDATALOAD // load second argument (b)
ADD // add a and b
PUSH1 0x00
MSTORE // store result in memory
PUSH1 0x20
PUSH1 0x00
RETURN // return 32 bytes from memory[0..31]
...
First we push the memory slot for the return value onto the stack. Then we load the two arguments a
and b
from the call data using CALLDATALOAD
. The ADD
opcode adds the two values on the stack. Finally, we store the result in memory and return it.
Commonly Used Opcodes
- SLOAD - Read storage (800 gas)
- SSTORE - Write storage (5k-20k gas)
- CALL - External call (700 gas + transfer costs)
- DELEGATECALL - Execute in caller’s context
- STATICCALL - Read-only external call
Each opcode has a specific gas cost associated with it. The gas cost is deducted from the total gas provided for the transaction.
Assembly Example
In solidity you can use inline assembly to write low-level code using opcodes. Here’s an example of using inline assembly to add two numbers:
pragma solidity ^0.8.0;
contract SimpleAdder {
function add(uint a, uint b) public pure returns (uint result) {
assembly {
result := add(a, b) // call EVM's ADD opcode directly
}
}
}
The assembly
block allows you to write low-level code using EVM opcodes directly. In this case, we use the add
opcode to add the two numbers.
Buy Why use Assembly?
- Gas Optimization: Inline assembly lets you skip some compiler overhead.
- Direct EVM Control: For working with memory, calldata or storage precisely.
- Understanding Low-Level Execution: Helps debug and optimize smart contracts.
When not?
- Complexity: Assembly is harder to maintain.
- Security Risks: More prone to bugs and vulnerabilities.
- Assembly bypasses solidity’s safety checks(overflow, bounds) etc.
Gas Mechanics (EIP-1559)
Gas is a unit that measures the amount of computational effort required to execute operations on the Ethereum network. Every operation that the EVM performs has a gas cost associated with it. When you send a transaction, you specify a gas limit and a gas price. The gas limit is the maximum amount of gas you are willing to spend on the transaction, and the gas price is how much you are willing to pay per unit of gas.
Total Fee = Base Fee + Priority Fee
Max Fee = Max Base Fee + Max Priority Fee
Let’s do a gas analysis of our SimpleAdder
contract’s, both assembly and high-level solidity version.
Writing tests is the best way to measure gas usage. Here’s an example using Hardhat:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Gas Comparison", function () {
let solidityAdder, asmAdder;
before(async () => {
const SimpleAdder = await ethers.getContractFactory("SimpleAdder");
solidityAdder = await SimpleAdder.deploy();
await solidityAdder.deployed();
const SimpleAdderAsm = await ethers.getContractFactory("SimpleAdderAsm");
asmAdder = await SimpleAdderAsm.deploy();
await asmAdder.deployed();
});
it("estimates gas for Solidity add()", async () => {
const gas = await solidityAdder.estimateGas.add(5, 7);
console.log("Solidity add() gas:", gas.toString());
});
it("estimates gas for Assembly add()", async () => {
const gas = await asmAdder.estimateGas.add(5, 7);
console.log("Assembly add() gas:", gas.toString());
});
});
When you run the above tests, you’ll see the gas consumption for both the high-level Solidity version and the inline assembly version of the add
function. You can compare the gas usage to see if using assembly provides any significant savings.
Solidity add() gas: 21628
Assembly add() gas: 21588
Not much significant difference for this simple adder example, but for more complex operations, assembly can lead to more noticeable gas savings.