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: 0xa1 Introduced: Frontier (EVM genesis) LOG1 emits a log entry with one indexed topic. This is the most common form for single-parameter event filtering, used extensively in token transfer events and simple state changes.

Specification

Stack Input:
offset (top)
length
topic0
Stack Output:
(none)
Gas Cost: 375 + 375 + (8 × data_length) + memory_expansion_cost Operation:
data = memory[offset : offset + length]
topic = stack.pop()
log_entry = { address: msg.sender, topics: [topic], data: data }
append log_entry to logs

Behavior

LOG1 pops three values from the stack:
  1. Offset: Starting position in memory (256-bit value)
  2. Length: Number of bytes to read from memory (256-bit value)
  3. Topic0: First indexed parameter (256-bit value)
The log entry contains one topic for efficient filtering while supporting arbitrary data.

Topic Values

Topics are stored as full 256-bit values. For dynamic types (strings, arrays, structs), the keccak256 hash is used as the topic:
event Transfer(address indexed from, address indexed to, uint256 value);
// Topic = keccak256("Transfer(address,indexed address,indexed uint256)")

event Named(string indexed name);
// Topic = keccak256(abi.encode(name))

Memory Expansion

Memory expands in 32-byte words beyond the current allocation, with associated gas costs.

Static Call Protection

LOG1 cannot execute in static call context (EIP-214).

Examples

Basic Topic Logging

import { handler_0xa1_LOG1 } from '@tevm/voltaire/evm/log';

const frame = createFrame({
  address: "0x1234567890123456789012345678901234567890",
  stack: [
    0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan,  // topic0
    0n,  // length
    0n,  // offset
  ],
  gasRemaining: 1000000n,
});

const err = handler_0xa1_LOG1(frame);
console.log(err); // null (success)
console.log(frame.logs[0].topics); // [0xaaa...aaan]
console.log(frame.gasRemaining); // 999250n (1000000 - 375 base - 375 topic)

Topic with Data

const frame = createFrame({
  address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  memory: new Map([
    [0, 0x00], [1, 0x00], [2, 0x00], [3, 0x10],  // 16 in bytes
  ]),
  stack: [
    0x1111111111111111111111111111111111111111111111111111111111111111n,
    4n,    // length
    0n,    // offset
  ],
  gasRemaining: 1000000n,
});

handler_0xa1_LOG1(frame);

const log = frame.logs[0];
console.log(log.topics); // [0x1111...1111n]
console.log(log.data);   // Uint8Array(4) [0, 0, 0, 16]
console.log(frame.gasRemaining); // 999633n (375 + 375 + 32 data + 3 memory)

Solidity Transfer Event

contract ERC20 {
  event Transfer(address indexed from, address indexed to, uint256 value);

  function transfer(address to, uint256 amount) public returns (bool) {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    balances[to] += amount;

    // Compiler generates LOG2 or LOG1 depending on indexed params
    emit Transfer(msg.sender, to, amount);
    return true;
  }
}

Named Event Log

event Named(string indexed name, string description);

contract NameRegistry {
  function register(string memory name, string memory description) public {
    names[msg.sender] = name;
    emit Named(name, description);
    // LOG1: topic = keccak256(abi.encode(name))
    //       data = abi.encode(description)
  }
}

ID-Based Event

event ItemCreated(uint256 indexed itemId);

contract ItemFactory {
  function create() public returns (uint256) {
    uint256 id = nextId++;
    items[id] = Item({ creator: msg.sender, timestamp: block.timestamp });
    emit ItemCreated(id);  // LOG1 with itemId as topic
    return id;
  }
}

Gas Cost

Base Cost: 375 gas Topic Cost: 375 gas (per topic, 1 for LOG1) Data Cost: 8 gas per byte Memory Expansion: Proportional to new memory range Examples:
  • Empty data: 375 + 375 = 750 gas
  • 1 byte: 750 + 8 = 758 gas
  • 32 bytes: 750 + 256 = 1006 gas
  • 64 bytes: 750 + 512 + 3 (memory expansion) = 1265 gas

Edge Cases

Topic Boundary Values

const frame = createFrame({
  stack: [
    (1n << 256n) - 1n,  // Max uint256 topic
    0n,                 // length
    0n,                 // offset
  ],
  gasRemaining: 1000000n,
});
handler_0xa1_LOG1(frame);
const log = frame.logs[0];
console.log(log.topics[0]); // (1n << 256n) - 1n (preserved)

Zero Topic

const frame = createFrame({
  stack: [0n, 0n, 0n],
  gasRemaining: 1000000n,
});
handler_0xa1_LOG1(frame);
const log = frame.logs[0];
console.log(log.topics[0]); // 0n

Large Data

const frame = createFrame({
  stack: [0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 5000n, 0n],
  gasRemaining: 100000n,
});
const err = handler_0xa1_LOG1(frame);
// Gas: 750 + 40000 (data) = 40750, exceeds 100000 after memory expansion
// Result: OutOfGas or success depending on memory costs

Stack Underflow

const frame = createFrame({ stack: [0n, 0n] });  // Missing topic
const err = handler_0xa1_LOG1(frame);
console.log(err); // { type: "StackUnderflow" }

Out of Gas

const frame = createFrame({
  stack: [0x1111111111111111111111111111111111111111111111111111111111111111n, 0n, 0n],
  gasRemaining: 749n,  // Not enough for base + topic cost
});
const err = handler_0xa1_LOG1(frame);
console.log(err); // { type: "OutOfGas" }

Common Usage

Event Filtering in Contracts

event LogIn(address indexed user);
event LogOut(address indexed user);

contract SessionManager {
  mapping(address => bool) public isLoggedIn;

  function login() public {
    isLoggedIn[msg.sender] = true;
    emit LogIn(msg.sender);  // LOG1: topic = msg.sender
  }

  function logout() public {
    isLoggedIn[msg.sender] = false;
    emit LogOut(msg.sender);  // LOG1: topic = msg.sender
  }
}

Off-Chain Filtering

// Listen for LogIn events from specific user
const logs = await getLogs({
  address: sessionManager.address,
  topics: [
    keccak256("LogIn(address)"),
    "0x1234567890123456789012345678901234567890",
  ]
});

// Returns only LogIn events where user matches the address

State Change Events

event Configured(uint256 indexed configId);

contract ConfigManager {
  function setConfig(uint256 id, bytes memory data) public {
    configs[id] = data;
    emit Configured(id);  // LOG1
  }
}

Security

Topic Hashing

For dynamic types, ensure consistent hashing:
event DataLogged(bytes32 indexed dataHash);

function logData(string memory data) public {
  // Correct: topic is keccak256 of the data
  emit DataLogged(keccak256(abi.encode(data)));
}

Static Call Context

LOG1 reverts in view/pure functions:
// WRONG: Reverts
function badView(address user) external view {
  emit LogIn(user);
}

// CORRECT: Use non-view function
function actuallyLogin(address user) external {
  emit LogIn(user);
}

Topic Value Limits

Topics are stored as full 256-bit values. No truncation or padding:
event LogSmallValue(uint8 indexed value);
// Topic stores full 256-bit value, not just uint8
// If value = 255, topic = 255n (with leading zeros)

Implementation

/**
 * LOG1 opcode (0xa1) - Emit log with 1 indexed topic
 */
export function handler_0xa1_LOG1(frame: FrameType): EvmError | null {
  if (frame.isStatic) {
    return { type: "WriteProtection" };
  }

  if (frame.stack.length < 3) {
    return { type: "StackUnderflow" };
  }

  const offset = frame.stack.pop();
  const length = frame.stack.pop();
  const topic0 = frame.stack.pop();

  if (offset > Number.MAX_SAFE_INTEGER || length > Number.MAX_SAFE_INTEGER) {
    return { type: "OutOfBounds" };
  }

  const offsetNum = Number(offset);
  const lengthNum = Number(length);

  // Gas: 375 base + 375 topic + 8 per byte data
  const logGas = 375n + 375n;
  const dataGas = BigInt(lengthNum) * 8n;
  const totalGas = logGas + dataGas;

  // Memory expansion
  if (lengthNum > 0) {
    const endByte = offsetNum + lengthNum;
    const newMemWords = Math.ceil(endByte / 32);
    const newMemSize = newMemWords * 32;
    const memExpansion = calculateMemoryExpansion(frame.memorySize, newMemSize);
    frame.memorySize = newMemSize;
    frame.gasRemaining -= BigInt(memExpansion);
  }

  frame.gasRemaining -= totalGas;
  if (frame.gasRemaining < 0n) {
    return { type: "OutOfGas" };
  }

  // Read data
  const data = new Uint8Array(lengthNum);
  for (let i = 0; i < lengthNum; i++) {
    data[i] = frame.memory.get(offsetNum + i) ?? 0;
  }

  // Create log entry
  const logEntry = {
    address: frame.address,
    topics: [topic0],
    data,
  };

  if (!frame.logs) frame.logs = [];
  frame.logs.push(logEntry);

  frame.pc += 1;
  return null;
}

Testing

import { describe, it, expect } from 'vitest';
import { handler_0xa1_LOG1 } from './0xa1_LOG1.js';

describe('LOG1 (0xa1)', () => {
  it('emits log with 1 topic and empty data', () => {
    const topic = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan;
    const frame = createFrame({
      stack: [topic, 0n, 0n],
      gasRemaining: 1000000n,
    });
    const err = handler_0xa1_LOG1(frame);
    expect(err).toBeNull();
    expect(frame.logs).toHaveLength(1);
    expect(frame.logs[0].topics).toEqual([topic]);
    expect(frame.gasRemaining).toBe(999250n);
  });

  it('emits log with topic and data', () => {
    const frame = createFrame({
      memory: new Map([[0, 0xde], [1, 0xad]]),
      stack: [0x1111n, 2n, 0n],
      gasRemaining: 1000000n,
    });
    handler_0xa1_LOG1(frame);
    const log = frame.logs[0];
    expect(log.topics).toEqual([0x1111n]);
    expect(log.data).toEqual(new Uint8Array([0xde, 0xad]));
  });

  it('returns WriteProtection in static context', () => {
    const frame = createFrame({ isStatic: true, stack: [0n, 0n, 0n] });
    const err = handler_0xa1_LOG1(frame);
    expect(err).toEqual({ type: "WriteProtection" });
  });

  it('returns StackUnderflow with 2 items', () => {
    const frame = createFrame({ stack: [0n, 0n] });
    const err = handler_0xa1_LOG1(frame);
    expect(err).toEqual({ type: "StackUnderflow" });
  });

  it('handles max uint256 topic', () => {
    const maxUint256 = (1n << 256n) - 1n;
    const frame = createFrame({
      stack: [maxUint256, 0n, 0n],
      gasRemaining: 1000000n,
    });
    handler_0xa1_LOG1(frame);
    expect(frame.logs[0].topics[0]).toBe(maxUint256);
  });
});

References