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: 0xa2
Introduced: Frontier (EVM genesis)
LOG2 emits a log entry with two indexed topics. This is the standard form for binary relationships like token transfers (from → to) or state transitions with two parameters.
Specification
Stack Input:
offset (top)
length
topic0
topic1
Stack Output:
Gas Cost: 375 + (2 × 375) + (8 × data_length) + memory_expansion_cost
Operation:
data = memory[offset : offset + length]
topic0 = stack.pop()
topic1 = stack.pop()
log_entry = { address: msg.sender, topics: [topic0, topic1], data: data }
append log_entry to logs
Behavior
LOG2 pops four values from the stack:
- Offset: Starting position in memory (256-bit value)
- Length: Number of bytes to read from memory (256-bit value)
- Topic0: First indexed parameter (256-bit value)
- Topic1: Second indexed parameter (256-bit value)
Topics are stored in the order they’re popped, enabling efficient filtering by either or both topics.
Topic Values
Both topics are preserved as full 256-bit values. For dynamic types, keccak256 hashes are used:
event Transfer(address indexed from, address indexed to, uint256 value);
// topic0 = from (address, zero-extended to 256 bits)
// topic1 = to (address, zero-extended to 256 bits)
// data = abi.encode(value)
Memory Expansion
Memory expands to word boundaries with associated gas costs.
Static Call Protection
LOG2 cannot execute in static call context (EIP-214).
Examples
Transfer Event (Most Common)
import { handler_0xa2_LOG2 } from '@tevm/voltaire/evm/log';
const frame = createFrame({
address: "0x1234567890123456789012345678901234567890",
stack: [
0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbn, // topic1 (to)
0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan, // topic0 (from)
0n, // length
0n, // offset
],
gasRemaining: 1000000n,
});
const err = handler_0xa2_LOG2(frame);
console.log(err); // null (success)
console.log(frame.logs[0].topics);
// [0xaaa...aaan, 0xbbb...bbbn]
console.log(frame.gasRemaining); // 999000n (375 + 750 topic cost)
Transfer with Value Data
const frame = createFrame({
address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
memory: new Map([
[0, 0x00], [1, 0x00], [2, 0x00], [3, 0x64], // 100 in decimal
]),
stack: [
0x2222222222222222222222222222222222222222222222222222222222222222n,
0x1111111111111111111111111111111111111111111111111111111111111111n,
4n, // length (value = 100)
0n, // offset
],
gasRemaining: 1000000n,
});
handler_0xa2_LOG2(frame);
const log = frame.logs[0];
console.log(log.topics);
// [0x1111...1111n, 0x2222...2222n]
console.log(log.data);
// Uint8Array(4) [0, 0, 0, 100]
console.log(frame.gasRemaining);
// 999383n (375 + 750 + 32 data + 3 memory)
ERC20 Token Transfer
pragma solidity ^0.8.0;
contract ERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
// Compiler generates LOG2 for this event
// topic0 = from (msg.sender)
// topic1 = to
// data = abi.encode(amount)
emit Transfer(msg.sender, to, amount);
return true;
}
}
Swap Event
event Swap(
address indexed sender,
address indexed recipient,
uint256 amount0In,
uint256 amount1Out
);
function swap(address to, uint256 minOut) public {
uint256 amountOut = getAmountOut(msg.value);
require(amountOut >= minOut);
// LOG2: topic0=sender, topic1=recipient
// data=abi.encode(amountIn, amountOut)
emit Swap(msg.sender, to, msg.value, amountOut);
}
State Transition Event
event StateChanged(address indexed from, address indexed to);
contract StateMachine {
mapping(bytes32 => address) public currentState;
function transition(bytes32 id, address newState) public {
address oldState = currentState[id];
currentState[id] = newState;
emit StateChanged(oldState, newState); // LOG2
}
}
Gas Cost
Base Cost: 375 gas
Topic Cost: 375 gas per topic = 750 gas (for 2 topics)
Data Cost: 8 gas per byte
Memory Expansion: Proportional to new memory range
Examples:
- Empty data: 375 + 750 = 1125 gas
- 1 byte: 1125 + 8 = 1133 gas
- 32 bytes: 1125 + 256 = 1381 gas
- 64 bytes: 1125 + 512 + 3 (memory expansion) = 1640 gas
- 256 bytes: 1125 + 2048 + 6 (memory expansion) = 3179 gas
Edge Cases
Topic Boundary Values
const frame = createFrame({
stack: [
(1n << 256n) - 1n, // Max uint256 topic1
0n, // Min uint256 topic0
0n, // length
0n, // offset
],
gasRemaining: 1000000n,
});
handler_0xa2_LOG2(frame);
const log = frame.logs[0];
console.log(log.topics); // [0n, (1n << 256n) - 1n]
Identical Topics
const topic = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefn;
const frame = createFrame({
stack: [topic, topic, 0n, 0n],
gasRemaining: 1000000n,
});
handler_0xa2_LOG2(frame);
const log = frame.logs[0];
console.log(log.topics); // [topic, topic] (allowed, both identical)
Large Data
const frame = createFrame({
stack: [0xfffn, 0xfffn, 10000n, 0n],
gasRemaining: 100000n,
});
const err = handler_0xa2_LOG2(frame);
// Gas: 1125 + 80000 (data) + memory expansion ≈ 81125
// Result: OutOfGas (insufficient)
Stack Underflow
const frame = createFrame({ stack: [0n, 0n, 0n] }); // Missing topic1
const err = handler_0xa2_LOG2(frame);
console.log(err); // { type: "StackUnderflow" }
Out of Gas
const frame = createFrame({
stack: [0xfffn, 0xfffn, 0n, 0n],
gasRemaining: 1124n, // Not enough for base + both topics
});
const err = handler_0xa2_LOG2(frame);
console.log(err); // { type: "OutOfGas" }
Common Usage
Event Filtering
event Transfer(address indexed from, address indexed to, uint256 value);
contract Token {
function transfer(address to, uint256 amount) public {
// ...
emit Transfer(msg.sender, to, amount);
}
}
Off-chain filtering:
// Listen for all transfers FROM a specific address
const logs = await provider.getLogs({
address: token.address,
topics: [
keccak256("Transfer(address,indexed address,indexed uint256)"),
"0xfrom_address" // First topic filter
]
});
// Listen for all transfers TO a specific address
const logsTo = await provider.getLogs({
address: token.address,
topics: [
keccak256("Transfer(address,indexed address,indexed uint256)"),
null, // Any from
"0xto_address" // Second topic filter
]
});
// Listen for transfers between specific addresses
const logsBetween = await provider.getLogs({
address: token.address,
topics: [
keccak256("Transfer(address,indexed address,indexed uint256)"),
"0xfrom_address", // Specific from
"0xto_address" // Specific to
]
});
Dual Authorization
event Approved(address indexed owner, address indexed spender, uint256 value);
contract ERC20 {
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approved(msg.sender, spender, amount); // LOG2
return true;
}
}
Pair Operations
event LiquidityAdded(
address indexed provider,
address indexed token,
uint256 amount
);
contract LiquidityPool {
function addLiquidity(address token, uint256 amount) public {
// ...
emit LiquidityAdded(msg.sender, token, amount); // LOG2
}
}
Security
Topic Filtering Security
Topics enable efficient filtering, but are visible off-chain:
// Sensitive data should NOT be in topics
event BadPractice(address indexed user, string indexed password);
// password hash is visible to anyone reading logs
// Better: hash dynamic data
event GoodPractice(address indexed user, bytes32 passwordHash);
Address Topic Semantics
When filtering by address topics, ensure zero-extension understanding:
event Log(address indexed addr);
// Topic = addr as uint256 (20 bytes zero-extended to 256 bits)
// Off-chain filtering must match zero-extended form
const logs = await getLogs({
topics: ["0x0000000000000000000000001234567890123456789012345678901234567890"]
});
Static Call Context
LOG2 reverts in view/pure functions:
// WRONG
function badView(address a, address b) external view {
emit SomeEvent(a, b); // Reverts
}
// CORRECT
function goodNonView(address a, address b) external {
emit SomeEvent(a, b); // Works
}
Implementation
/**
* LOG2 opcode (0xa2) - Emit log with 2 indexed topics
*/
export function handler_0xa2_LOG2(frame: FrameType): EvmError | null {
if (frame.isStatic) {
return { type: "WriteProtection" };
}
if (frame.stack.length < 4) {
return { type: "StackUnderflow" };
}
const offset = frame.stack.pop();
const length = frame.stack.pop();
const topic0 = frame.stack.pop();
const topic1 = 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 + 750 topics + 8 per byte data
const logGas = 375n + 750n;
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],
data,
};
if (!frame.logs) frame.logs = [];
frame.logs.push(logEntry);
frame.pc += 1;
return null;
}
Testing
import { describe, it, expect } from 'vitest';
import { handler_0xa2_LOG2 } from './0xa2_LOG2.js';
describe('LOG2 (0xa2)', () => {
it('emits log with 2 topics and empty data', () => {
const topic0 = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan;
const topic1 = 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbn;
const frame = createFrame({
stack: [topic1, topic0, 0n, 0n],
gasRemaining: 1000000n,
});
const err = handler_0xa2_LOG2(frame);
expect(err).toBeNull();
expect(frame.logs).toHaveLength(1);
expect(frame.logs[0].topics).toEqual([topic0, topic1]);
expect(frame.gasRemaining).toBe(998875n);
});
it('emits log with 2 topics and data', () => {
const frame = createFrame({
memory: new Map([[0, 0xde], [1, 0xad]]),
stack: [0x2222n, 0x1111n, 2n, 0n],
gasRemaining: 1000000n,
});
handler_0xa2_LOG2(frame);
const log = frame.logs[0];
expect(log.topics).toEqual([0x1111n, 0x2222n]);
expect(log.data).toEqual(new Uint8Array([0xde, 0xad]));
});
it('returns WriteProtection in static context', () => {
const frame = createFrame({ isStatic: true, stack: [0n, 0n, 0n, 0n] });
const err = handler_0xa2_LOG2(frame);
expect(err).toEqual({ type: "WriteProtection" });
});
it('returns StackUnderflow with 3 items', () => {
const frame = createFrame({ stack: [0n, 0n, 0n] });
const err = handler_0xa2_LOG2(frame);
expect(err).toEqual({ type: "StackUnderflow" });
});
it('handles max values for both topics', () => {
const maxUint256 = (1n << 256n) - 1n;
const frame = createFrame({
stack: [maxUint256, maxUint256, 0n, 0n],
gasRemaining: 1000000n,
});
handler_0xa2_LOG2(frame);
expect(frame.logs[0].topics).toEqual([maxUint256, maxUint256]);
});
it('expands memory correctly with large data', () => {
const frame = createFrame({
stack: [0xfffn, 0xfffn, 100n, 50n],
gasRemaining: 1000000n,
});
handler_0xa2_LOG2(frame);
// Memory expands to cover offset 50 + length 100 = 150 bytes
// Word-aligned to 160 bytes (5 words * 32)
expect(frame.memorySize).toBe(160);
});
});
References