Skip to main content
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Overview

Opcode: 0x54 Introduced: Frontier (EVM genesis) Updated: Berlin (EIP-2929, cold/warm tracking) SLOAD reads a 256-bit value from an account’s persistent storage. The gas cost depends on whether the storage slot was previously accessed in the transaction (warm) or not (cold). This operation is essential for reading contract state: balances, permissions, prices, and any data that must persist across transactions.

Specification

Stack Input:
key (storage slot address)
Stack Output:
value (256-bit value at slot, or 0 if uninitialized)
Gas Cost:
  • 100 gas - Warm access (already accessed in transaction)
  • 2100 gas - Cold access (first access, EIP-2929)
Operation:
slot = pop()
value = storage[msg.sender][slot]  // 0 if slot never written
push(value)

Behavior

SLOAD retrieves the current value stored at a key in the calling contract’s account storage:
  1. Pop key from stack (256-bit unsigned integer)
  2. Query host for storage value at contract address + key
  3. Return value from host (0 if slot never written or cleared)
  4. Push result to stack
  5. Track access for cold/warm metering (EIP-2929)
  6. Increment PC

Cold vs Warm Access

First access in transaction (cold): 2100 gas
SLOAD(key)  // 2100 gas - cold access, not yet seen
Subsequent accesses (warm): 100 gas
SLOAD(key)  // 2100 gas first time
SLOAD(key)  // 100 gas second time (warm)
Access list (EIP-2930): Can pre-warm slots
// Access list declaration in transaction
[{ address: contract, storageKeys: [key] }]
// SLOAD uses warm cost even on first access

Uninitialized Slots

Slots never written return 0:
SLOAD(0xFFFFFFFF)  // Returns 0 (never written)

Examples

Basic Storage Read

import { sload } from '@voltaire/evm/storage';
import { createFrame } from '@voltaire/evm/Frame';
import { createMemoryHost } from '@voltaire/evm/Host';

const host = createMemoryHost();
const frame = createFrame({
  stack: [0x42n],  // Key to read
  gasRemaining: 3000n,
  address: contractAddr,
});

// Pre-populate storage
host.setStorage(contractAddr, 0x42n, 0x1337n);

// Execute SLOAD
const error = sload(frame, host);

console.log(frame.stack);        // [0x1337n]
console.log(frame.gasRemaining); // 2900n (3000 - 100 warm)
console.log(error);              // null

Cold Access Tracking

const host = createMemoryHost();
host.setStorage(contractAddr, 0x42n, 0x1337n);
host.setStorage(contractAddr, 0x43n, 0x2222n);

// First access to slot 0x42 (cold)
let frame = createFrame({
  stack: [0x42n],
  gasRemaining: 3000n,
  address: contractAddr,
});
sload(frame, host);
console.log(frame.gasRemaining);  // 900n (3000 - 2100 cold)

// Second access to slot 0x42 (warm, cached)
frame = createFrame({
  stack: [0x42n],
  gasRemaining: 3000n,
  address: contractAddr,
});
sload(frame, host);
console.log(frame.gasRemaining);  // 2900n (3000 - 100 warm)

// First access to slot 0x43 (cold, different slot)
frame = createFrame({
  stack: [0x43n],
  gasRemaining: 3000n,
  address: contractAddr,
});
sload(frame, host);
console.log(frame.gasRemaining);  // 900n (3000 - 2100 cold)

Mapping Access

// Smart contract mapping
mapping(address => uint256) public balances;

// Reading mapping[addr]
// Solidity computes: keccak256(abi.encode(addr, 1)) → storage slot
// Then SLOAD retrieves value at that slot

function getBalance(address user) public view returns (uint256) {
  return balances[user];  // SLOAD with computed key
}

Nested Structures

// Reading from dynamic storage arrays
// array[index] → SLOAD with computed offset
// Requires: baseSlot + (32 * index) for 32-byte elements

const baseSlot = 5n;  // Array stored at slot 5
const index = 10n;
const storageKey = baseSlot + (32n * index);  // Compute slot

frame = createFrame({
  stack: [storageKey],
  gasRemaining: 3000n,
  address: contractAddr,
});
sload(frame, host);

Gas Cost

Cost Matrix:
Access TypeGasEIPNotes
Warm100EIP-2929Seen before in transaction
Cold2100EIP-2929First access in transaction
Pre-warmed100EIP-2930Via access list
Optimization: SLOAD is ~21x more expensive on cold access. Batch accesses or use access lists for known storage reads.

Edge Cases

Uninitialized Slot

const frame = createFrame({
  stack: [0xDEADBEEFn],
  gasRemaining: 3000n,
  address: contractAddr,
});

// Slot never written - returns 0
sload(frame, host);
console.log(frame.stack);  // [0n]

Max Uint256 Value

const MAX = (1n << 256n) - 1n;
host.setStorage(contractAddr, 0x1n, MAX);

const frame = createFrame({
  stack: [0x1n],
  gasRemaining: 3000n,
  address: contractAddr,
});

sload(frame, host);
console.log(frame.stack);  // [MAX]

Stack Boundaries

// Reading with key = 0
const frame = createFrame({
  stack: [0n],  // Slot 0
  gasRemaining: 3000n,
  address: contractAddr,
});
sload(frame, host);

// Reading with key = max
const maxFrame = createFrame({
  stack: [(1n << 256n) - 1n],
  gasRemaining: 3000n,
  address: contractAddr,
});
sload(maxFrame, host);

Insufficient Gas

const frame = createFrame({
  stack: [0x42n],
  gasRemaining: 50n,  // < 100 (warm cost)
  address: contractAddr,
});

const error = sload(frame, host);
console.log(error);  // { type: "OutOfGas" }
console.log(frame.pc);  // 0 (unchanged, not executed)

Common Usage

State Variable Access

contract Counter {
  uint256 public count;  // Slot 0

  function getCount() public view returns (uint256) {
    return count;  // SLOAD(0)
  }

  function increment() public {
    count++;  // SLOAD(0) + ADD + SSTORE(0)
  }
}

Mapping Lookups

contract Bank {
  mapping(address => uint256) public balances;

  function getBalance(address user) public view returns (uint256) {
    return balances[user];  // SLOAD with keccak computed key
  }

  function transfer(address to, uint256 amount) public {
    uint256 myBalance = balances[msg.sender];  // SLOAD cold (2100)
    require(myBalance >= amount, "insufficient balance");

    balances[msg.sender] = myBalance - amount;  // SLOAD warm (100)
    balances[to] += amount;
  }
}

Multi-Read Optimization

// Inefficient: Multiple SLOAD cold accesses
function inefficient(address user) public view returns (uint256, uint256, uint256) {
  return (
    balances[user],     // SLOAD cold (2100)
    approved[user],     // SLOAD cold (2100)
    lastUpdate[user]    // SLOAD cold (2100)
  );
}

// Efficient: Cache in memory (MSTORE is cheap)
function efficient(address user) public view returns (uint256, uint256, uint256) {
  uint256 bal = balances[user];     // SLOAD cold (2100)
  uint256 app = approved[user];     // SLOAD cold (2100) - new slot
  uint256 last = lastUpdate[user];  // SLOAD cold (2100) - new slot
  return (bal, app, last);
}

Access List Warm-up

// Solidity: Declare access list in transaction
const tx = {
  to: contractAddr,
  data: encodeFunctionCall("transfer", [to, amount]),
  accessList: [
    {
      address: contractAddr,
      storageKeys: [
        0x0n,  // balances mapping base
        0x1n,  // approved mapping base
      ]
    }
  ]
};

// SLOAD now uses 100 gas (warm) even on first access

Implementation

import * as Frame from "../../Frame/index.js";
import { ColdSload } from "../../../primitives/GasConstants/BrandedGasConstants/constants.js";

/**
 * SLOAD (0x54) - Load word from storage
 *
 * Stack:
 *   in: key
 *   out: value
 *
 * Gas: 100 (warm) or 2100 (cold) - EIP-2929
 */
export function sload(frame, host) {
  // Pop key from stack
  const keyResult = Frame.popStack(frame);
  if (keyResult.error) return keyResult.error;
  const key = keyResult.value;

  // Note: EIP-2929 access list tracking for warm/cold slots
  // For now, assume cold access (worst case)
  const gasCost = ColdSload;

  const gasError = Frame.consumeGas(frame, gasCost);
  if (gasError) return gasError;

  // Load from storage via host
  const value = host.getStorage(frame.address, key);

  // Push value onto stack
  const pushError = Frame.pushStack(frame, value);
  if (pushError) return pushError;

  frame.pc += 1;
  return null;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { sload } from './0x54_SLOAD.js';
import { createFrame } from '../../Frame/index.js';
import { createMemoryHost } from '../../Host/createMemoryHost.js';
import { from as addressFrom } from '../../../primitives/Address/index.js';

describe('SLOAD (0x54)', () => {
  it('loads value from storage', () => {
    const host = createMemoryHost();
    const addr = addressFrom("0x1234567890123456789012345678901234567890");
    host.setStorage(addr, 0x42n, 0x1337n);

    const frame = createFrame({
      stack: [0x42n],
      gasRemaining: 10000n,
      address: addr,
    });

    expect(sload(frame, host)).toBeNull();
    expect(frame.stack).toEqual([0x1337n]);
    expect(frame.pc).toBe(1);
  });

  it('loads zero for uninitialized storage', () => {
    const host = createMemoryHost();
    const addr = addressFrom("0x1234567890123456789012345678901234567890");

    const frame = createFrame({
      stack: [0xFFFFFFFFFn],
      gasRemaining: 10000n,
      address: addr,
    });

    expect(sload(frame, host)).toBeNull();
    expect(frame.stack).toEqual([0n]);
  });

  it('isolates storage by address', () => {
    const host = createMemoryHost();
    const addr1 = addressFrom("0x1111111111111111111111111111111111111111");
    const addr2 = addressFrom("0x2222222222222222222222222222222222222222");

    host.setStorage(addr1, 0x42n, 0xAAAAn);
    host.setStorage(addr2, 0x42n, 0xBBBBn);

    let frame = createFrame({
      stack: [0x42n],
      gasRemaining: 10000n,
      address: addr1,
    });
    expect(sload(frame, host)).toBeNull();
    expect(frame.stack).toEqual([0xAAAAn]);

    frame = createFrame({
      stack: [0x42n],
      gasRemaining: 10000n,
      address: addr2,
    });
    expect(sload(frame, host)).toBeNull();
    expect(frame.stack).toEqual([0xBBBBn]);
  });

  it('consumes 2100 gas on cold access', () => {
    const host = createMemoryHost();
    const addr = addressFrom("0x1234567890123456789012345678901234567890");
    host.setStorage(addr, 0x42n, 0x1337n);

    const frame = createFrame({
      stack: [0x42n],
      gasRemaining: 5000n,
      address: addr,
    });

    expect(sload(frame, host)).toBeNull();
    expect(frame.gasRemaining).toBe(2900n);  // 5000 - 2100
  });

  it('returns StackUnderflow on empty stack', () => {
    const host = createMemoryHost();
    const frame = createFrame({
      stack: [],
      gasRemaining: 10000n,
      address: addressFrom("0x1111111111111111111111111111111111111111"),
    });

    expect(sload(frame, host)).toEqual({ type: "StackUnderflow" });
  });

  it('returns OutOfGas when insufficient gas', () => {
    const host = createMemoryHost();
    const frame = createFrame({
      stack: [0x42n],
      gasRemaining: 50n,
      address: addressFrom("0x1111111111111111111111111111111111111111"),
    });

    expect(sload(frame, host)).toEqual({ type: "OutOfGas" });
  });

  it('returns StackOverflow when stack full', () => {
    const host = createMemoryHost();
    const fullStack = new Array(1024).fill(0n);

    const frame = createFrame({
      stack: fullStack,
      gasRemaining: 10000n,
      address: addressFrom("0x1111111111111111111111111111111111111111"),
    });

    expect(sload(frame, host)).toEqual({ type: "StackOverflow" });
  });
});

Security

State Immutability During Reads

SLOAD is read-only and safe in any context (even static calls). It cannot modify state, only query it:
function read(address user) public view returns (uint256) {
  return balances[user];  // Always safe, read-only
}

Reentrancy Vulnerability (When Used with State Changes)

SLOAD itself is safe, but reading and then writing creates reentrancy windows:
// VULNERABLE: Read-check-write pattern
function transfer(address to, uint256 amount) public {
  uint256 balance = balances[msg.sender];  // SLOAD
  require(balance >= amount);

  balances[msg.sender] = balance - amount;  // SSTORE

  (bool ok, ) = to.call("");  // REENTERS HERE
  // Attacker re-enters before balance updated
}

// SAFE: Checks-Effects-Interactions pattern
function transfer(address to, uint256 amount) public {
  require(balances[msg.sender] >= amount);
  balances[msg.sender] -= amount;  // SSTORE first
  (bool ok, ) = to.call("");  // Reenter safely
  require(ok, "transfer failed");
}

Access List Validation

Ensure access lists match actual storage accessed:
// Declared in access list
accessList: [{ address: token, storageKeys: [slot0, slot1] }]

// But code accesses different slot
host.getStorage(token, 0xFF)  // Slot 0xFF not in access list!
// Will cost 2100 gas instead of expected 100

Gas Cost Variation

Cold/warm access affects gas accounting for batch operations:
function batchTransfer(address[] calldata users, uint256[] calldata amounts) public {
  for (uint i = 0; i < users.length; i++) {
    uint256 bal = balances[users[i]];  // Variable cost!
    // First unique user: 2100 gas (cold)
    // Repeated user: 100 gas (warm)
  }
}

Benchmarks

Access cost comparison:
  • Warm SLOAD: 100 gas
  • Cold SLOAD: 2100 gas (21x more expensive)
  • MLOAD (memory): 3 gas (67x cheaper than warm)
  • L1 cache: ~0.5ns vs ~100ns for cold storage
Practical implications:
// Reading 100 values from same slot
SLOAD(key)  // 2100 gas first time
// Keep in stack/memory (cheap operations)
// 99 more stack operations at 3 gas each ≈ 297 gas total
// Total: 2100 + 297 = 2397 gas

// vs reading from cold storage 100 times
// 100 × 2100 = 210,000 gas (86x more expensive!)

References