Skip to main content

Overview

A Frame represents a single EVM execution context. When a smart contract executes, it runs within a frame that maintains:
  • Stack - 1024-element LIFO stack of 256-bit values
  • Memory - Sparse byte-addressable scratch space
  • Gas - Remaining gas for execution
  • Call context - Caller, address, value, calldata
Each CALL, DELEGATECALL, STATICCALL, CREATE, or CREATE2 creates a new nested frame. The EVM supports up to 1024 call depth.

Type Definition

import type { BrandedFrame } from 'voltaire/evm/Frame';

// BrandedFrame structure
type BrandedFrame = {
  // Stack (max 1024 items)
  stack: bigint[];

  // Memory (sparse map)
  memory: Map<number, number>;
  memorySize: number; // Word-aligned size

  // Execution state
  pc: number;           // Program counter
  gasRemaining: bigint;
  bytecode: Uint8Array;

  // Call context
  caller: Address;
  address: Address;
  value: bigint;
  calldata: Uint8Array;
  output: Uint8Array;
  returnData: Uint8Array;

  // Flags
  stopped: boolean;
  reverted: boolean;
  isStatic: boolean;

  // Other
  authorized: bigint | null;
  callDepth: number;

  // Optional: hardfork, access lists, block context, logs...
};

Creating a Frame

import { Frame } from 'voltaire/evm/Frame';

// Create with defaults
const frame = Frame();

// Create with parameters
const frame = Frame({
  bytecode: new Uint8Array([0x60, 0x01, 0x60, 0x02, 0x01]), // PUSH1 1, PUSH1 2, ADD
  gas: 100000n,
  caller: callerAddress,
  address: contractAddress,
  value: 0n,
  calldata: new Uint8Array([]),
  isStatic: false,
});

Frame Parameters

ParameterTypeDefaultDescription
bytecodeUint8Array[]Contract bytecode to execute
gasbigint1000000nInitial gas allocation
callerAddresszero addressMessage sender (msg.sender)
addressAddresszero addressCurrent contract address
valuebigint0nWei transferred (msg.value)
calldataUint8Array[]Input data (msg.data)
isStaticbooleanfalseStatic call (no state modifications)
stackbigint[][]Initial stack state
gasRemainingbigintgasOverride gas (for resuming)

Stack Operations

pushStack

Push a 256-bit value onto the stack.
import { Frame, pushStack } from 'voltaire/evm/Frame';

const frame = Frame();
const error = pushStack(frame, 42n);

if (error) {
  // error.type === "StackOverflow" when stack has 1024 items
  console.error(error.type);
}

popStack

Pop the top value from the stack.
import { Frame, popStack, pushStack } from 'voltaire/evm/Frame';

const frame = Frame();
pushStack(frame, 100n);
pushStack(frame, 200n);

const result = popStack(frame);
if (result.error) {
  // result.error.type === "StackUnderflow" when stack is empty
  console.error(result.error.type);
} else {
  console.log(result.value); // 200n (LIFO)
}

peekStack

Read a stack value without removing it.
import { Frame, peekStack, pushStack } from 'voltaire/evm/Frame';

const frame = Frame();
pushStack(frame, 10n);
pushStack(frame, 20n);
pushStack(frame, 30n);

const top = peekStack(frame, 0);     // 30n (top)
const second = peekStack(frame, 1);  // 20n
const third = peekStack(frame, 2);   // 10n

Gas Operations

consumeGas

Deduct gas from the frame. Returns OutOfGas error if insufficient.
import { Frame, consumeGas } from 'voltaire/evm/Frame';

const frame = Frame({ gas: 100n });

consumeGas(frame, 30n);  // gasRemaining: 70n
consumeGas(frame, 50n);  // gasRemaining: 20n

const error = consumeGas(frame, 50n);
if (error) {
  console.log(error.type);       // "OutOfGas"
  console.log(frame.gasRemaining); // 0n
}

Memory Operations

writeMemory

Write a byte to memory. Memory expands in 32-byte words.
import { Frame, writeMemory } from 'voltaire/evm/Frame';

const frame = Frame();
writeMemory(frame, 0, 0xde);
writeMemory(frame, 1, 0xad);
writeMemory(frame, 2, 0xbe);
writeMemory(frame, 3, 0xef);

console.log(frame.memorySize); // 32 (word-aligned)

readMemory

Read a byte from memory. Uninitialized memory returns 0.
import { Frame, readMemory, writeMemory } from 'voltaire/evm/Frame';

const frame = Frame();
writeMemory(frame, 100, 0xff);

console.log(readMemory(frame, 100)); // 255 (0xff)
console.log(readMemory(frame, 0));   // 0 (uninitialized)

memoryExpansionCost

Calculate gas cost for memory expansion.
import { Frame, memoryExpansionCost, consumeGas, writeMemory } from 'voltaire/evm/Frame';

const frame = Frame({ gas: 1000n });

// Calculate cost before expanding
const cost = memoryExpansionCost(frame, 64);
consumeGas(frame, cost);

// Now safe to write
writeMemory(frame, 63, 0xff);
Memory cost formula: 3n + n²/512 where n is word count. This quadratic growth prevents memory DoS attacks.

Error Types

type EvmError =
  | { type: "StackOverflow" }      // Stack exceeds 1024 items
  | { type: "StackUnderflow" }     // Pop/peek from empty stack
  | { type: "OutOfGas" }           // Insufficient gas
  | { type: "OutOfBounds" }        // Invalid memory access
  | { type: "InvalidJump" }        // Jump to non-JUMPDEST
  | { type: "InvalidOpcode" }      // Unknown opcode
  | { type: "RevertExecuted" }     // REVERT opcode executed
  | { type: "CallDepthExceeded" }  // Call depth > 1024
  | { type: "WriteProtection" }    // State modification in STATICCALL
  | { type: "InsufficientBalance" }
  | { type: "NotImplemented"; message: string };

Arithmetic Methods

Frames include bound arithmetic methods for opcodes 0x01-0x0b:
const frame = Frame();
pushStack(frame, 10n);
pushStack(frame, 20n);

const error = frame.add();  // Stack: [30n]

// Available methods:
// frame.add(), frame.mul(), frame.sub(), frame.div(), frame.sdiv()
// frame.mod(), frame.smod(), frame.addmod(), frame.mulmod()
// frame.exp(), frame.signextend()

Complete Example

import { Frame, pushStack, popStack, consumeGas, writeMemory, readMemory } from 'voltaire/evm/Frame';
import { Address } from 'voltaire/primitives/Address';

// Simulate ADD opcode execution
const frame = Frame({
  bytecode: new Uint8Array([0x60, 0x0a, 0x60, 0x14, 0x01]), // PUSH1 10, PUSH1 20, ADD
  gas: 21000n,
  caller: Address("0x1111111111111111111111111111111111111111"),
  address: Address("0x2222222222222222222222222222222222222222"),
});

// PUSH1 10 (gas: 3)
consumeGas(frame, 3n);
pushStack(frame, 10n);
frame.pc += 2;

// PUSH1 20 (gas: 3)
consumeGas(frame, 3n);
pushStack(frame, 20n);
frame.pc += 2;

// ADD (gas: 3)
consumeGas(frame, 3n);
const error = frame.add();
frame.pc += 1;

// Result
const result = popStack(frame);
console.log(result.value);        // 30n
console.log(frame.gasRemaining);  // 20991n

API Reference

FunctionDescription
Frame(params?)Create new execution frame
pushStack(frame, value)Push bigint onto stack
popStack(frame)Pop and return top value
peekStack(frame, index)Read value at depth index
consumeGas(frame, amount)Deduct gas from frame
readMemory(frame, offset)Read byte from memory
writeMemory(frame, offset, value)Write byte to memory
memoryExpansionCost(frame, endBytes)Calculate expansion gas