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: 0xa3 Introduced: Frontier (EVM genesis) LOG3 emits a log entry with three indexed topics. This enables filtering complex events with multiple dimensional parameters, such as marketplace events involving buyer, seller, and item.

Specification

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

Behavior

LOG3 pops five 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)
  4. Topic1: Second indexed parameter (256-bit value)
  5. Topic2: Third indexed parameter (256-bit value)
Topics enable efficient three-dimensional filtering for complex event relationships.

Topic Values

All three topics are preserved as full 256-bit values. For dynamic types, keccak256 hashes apply.

Memory Expansion

Memory expands in 32-byte word increments with proportional gas costs.

Static Call Protection

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

Examples

Marketplace Event

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

const frame = createFrame({
  address: "0x1234567890123456789012345678901234567890",
  stack: [
    0xccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccn,  // topic2 (item)
    0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbn,  // topic1 (seller)
    0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan,  // topic0 (buyer)
    0n,  // length
    0n,  // offset
  ],
  gasRemaining: 1000000n,
});

const err = handler_0xa3_LOG3(frame);
console.log(err); // null (success)
console.log(frame.logs[0].topics.length); // 3
console.log(frame.gasRemaining);
// 999000n - 375 (base) - 1125 (3 topics)

Marketplace Transaction with Price

const frame = createFrame({
  address: "0xmarketplace",
  memory: new Map([
    [0, 0x00], [1, 0x00], [2, 0x01], [3, 0x00],  // Price: 256 wei
  ]),
  stack: [
    0xitem_id,
    0xseller,
    0xbuyer,
    4n,    // length (price bytes)
    0n,    // offset
  ],
  gasRemaining: 1000000n,
});

handler_0xa3_LOG3(frame);

const log = frame.logs[0];
console.log(log.topics.length); // 3
console.log(log.data); // Price encoded
console.log(frame.gasRemaining);
// 999000n - 1125 (topics) - 32 (data) - 3 (memory)

NFT Transfer Event

event Transfer(
  address indexed from,
  address indexed to,
  uint256 indexed tokenId
);

contract NFT {
  function transfer(address to, uint256 tokenId) public {
    require(balances[msg.sender][tokenId] > 0);
    balances[msg.sender][tokenId]--;
    balances[to][tokenId]++;

    // Compiler generates LOG3
    // topic0 = from
    // topic1 = to
    // topic2 = tokenId
    // data = (empty for ERC721)
    emit Transfer(msg.sender, to, tokenId);
  }
}

Approval with Token Event

event ApprovalForToken(
  address indexed owner,
  address indexed spender,
  address indexed token,
  uint256 amount
);

contract ApprovalManager {
  function approveForToken(
    address token,
    address spender,
    uint256 amount
  ) public {
    approvals[msg.sender][spender][token] = amount;
    emit ApprovalForToken(msg.sender, spender, token, amount);  // LOG3
  }
}

Order Placed Event

event OrderPlaced(
  address indexed buyer,
  address indexed seller,
  bytes32 indexed orderId,
  uint256 amount
);

contract OrderBook {
  function placeOrder(
    address seller,
    bytes32 orderId,
    uint256 amount
  ) public {
    orders[orderId] = Order({
      buyer: msg.sender,
      seller: seller,
      amount: amount,
      status: OrderStatus.PENDING
    });

    emit OrderPlaced(msg.sender, seller, orderId, amount);  // LOG3
  }
}

Gas Cost

Base Cost: 375 gas Topic Cost: 375 gas per topic = 1125 gas (for 3 topics) Data Cost: 8 gas per byte Memory Expansion: Proportional to new memory range Examples:
  • Empty data: 375 + 1125 = 1500 gas
  • 1 byte: 1500 + 8 = 1508 gas
  • 32 bytes: 1500 + 256 = 1756 gas
  • 64 bytes: 1500 + 512 + 3 = 2015 gas
  • 256 bytes: 1500 + 2048 + 6 = 3554 gas

Edge Cases

All Topics Identical

const topic = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefn;
const frame = createFrame({
  stack: [topic, topic, topic, 0n, 0n],
  gasRemaining: 1000000n,
});
handler_0xa3_LOG3(frame);
const log = frame.logs[0];
console.log(log.topics); // [topic, topic, topic] (all identical, allowed)

Mixed Topic Values

const frame = createFrame({
  stack: [
    (1n << 256n) - 1n,  // Max value
    0n,                 // Min value
    0x1234567890abcdefn, // Mixed
    0n,
    0n,
  ],
  gasRemaining: 1000000n,
});
handler_0xa3_LOG3(frame);
const log = frame.logs[0];
console.log(log.topics); // [0x1234..., 0n, (1n << 256n) - 1n]

Large Data with Topics

const frame = createFrame({
  stack: [
    0xfff,
    0xfff,
    0xfff,
    5000n,  // length
    0n,     // offset
  ],
  gasRemaining: 100000n,
});
const err = handler_0xa3_LOG3(frame);
// Gas: 1500 + 40000 (data) + memory expansion ≈ 41500
// Result: OutOfGas

Stack Underflow

const frame = createFrame({ stack: [0n, 0n, 0n, 0n] });  // Only 4 items
const err = handler_0xa3_LOG3(frame);
console.log(err); // { type: "StackUnderflow" }

Out of Gas

const frame = createFrame({
  stack: [0xfff, 0xfff, 0xfff, 0n, 0n],
  gasRemaining: 1499n,  // Not enough for base + all topics
});
const err = handler_0xa3_LOG3(frame);
console.log(err); // { type: "OutOfGas" }

Common Usage

Multi-Dimensional Filtering

event Trade(
  address indexed trader,
  address indexed token,
  address indexed counterparty,
  uint256 amount
);

contract DEX {
  function swapExactIn(
    address token,
    address counterparty,
    uint256 amountIn
  ) public {
    // ... swap logic
    emit Trade(msg.sender, token, counterparty, amountIn);  // LOG3
  }
}
Off-chain filtering:
// Listen for all trades by a specific trader
const logs = await provider.getLogs({
  address: dex.address,
  topics: [
    keccak256("Trade(address,indexed address,indexed address,indexed uint256)"),
    "0xtrader_address"
  ]
});

// Listen for trades with specific token
const logsWithToken = await provider.getLogs({
  address: dex.address,
  topics: [
    keccak256("Trade(...)"),
    null,              // Any trader
    "0xtoken_address", // Specific token
    null               // Any counterparty
  ]
});

// Listen for trades between specific parties
const logsBetween = await provider.getLogs({
  address: dex.address,
  topics: [
    keccak256("Trade(...)"),
    "0xtrader_address",
    null,
    "0xcounterparty_address"
  ]
});

Complex State Transitions

event StateTransition(
  address indexed user,
  bytes32 indexed fromState,
  bytes32 indexed toState,
  string reason
);

contract StateMachine {
  mapping(address => bytes32) public userState;

  function transitionState(bytes32 newState, string memory reason) public {
    bytes32 oldState = userState[msg.sender];
    userState[msg.sender] = newState;

    emit StateTransition(msg.sender, oldState, newState, reason);  // LOG3
  }
}

Authorization Events

event Authorization(
  address indexed grantor,
  address indexed grantee,
  address indexed resource,
  uint256 permissions
);

contract AccessControl {
  function grant(address grantee, address resource, uint256 perms) public {
    permissions[msg.sender][grantee][resource] = perms;
    emit Authorization(msg.sender, grantee, resource, perms);  // LOG3
  }
}

Security

Topic Visibility

All topics are visible off-chain. Do not include sensitive data:
// BAD: Private data in topics
event BadLog(address indexed user, string indexed password);

// GOOD: Hash sensitive data
event GoodLog(address indexed user, bytes32 passwordHash);

Filtering Semantics

Ensure consistent topic ordering and filtering:
event Swap(
  address indexed buyer,
  address indexed seller,
  address indexed token
);

// Off-chain: must match exact parameter order
// Topics: [buyer_hash, seller_hash, token]

Static Call Context

LOG3 reverts in view/pure functions:
// WRONG
function badView(address a, address b, address c) external view {
  emit Event(a, b, c);  // Reverts
}

// CORRECT
function goodNonView(address a, address b, address c) external {
  emit Event(a, b, c);  // Works
}

Implementation

/**
 * LOG3 opcode (0xa3) - Emit log with 3 indexed topics
 */
export function handler_0xa3_LOG3(frame: FrameType): EvmError | null {
  if (frame.isStatic) {
    return { type: "WriteProtection" };
  }

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

  const offset = frame.stack.pop();
  const length = frame.stack.pop();
  const topic0 = frame.stack.pop();
  const topic1 = frame.stack.pop();
  const topic2 = 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 + 1125 topics + 8 per byte data
  const logGas = 375n + 1125n;
  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, topic1, topic2],
    data,
  };

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

  frame.pc += 1;
  return null;
}

Testing

import { describe, it, expect } from 'vitest';
import { handler_0xa3_LOG3 } from './0xa3_LOG3.js';

describe('LOG3 (0xa3)', () => {
  it('emits log with 3 topics and empty data', () => {
    const topic0 = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan;
    const topic1 = 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbn;
    const topic2 = 0xccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccn;
    const frame = createFrame({
      stack: [topic2, topic1, topic0, 0n, 0n],
      gasRemaining: 1000000n,
    });
    const err = handler_0xa3_LOG3(frame);
    expect(err).toBeNull();
    expect(frame.logs).toHaveLength(1);
    expect(frame.logs[0].topics).toEqual([topic0, topic1, topic2]);
    expect(frame.gasRemaining).toBe(998500n);
  });

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

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

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

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

References