Skip to main content

Overview

Opcode: 0xa0 Introduced: Frontier (EVM genesis) LOG0 emits a log entry with no indexed topics. Only the event data (non-indexed parameters) is logged, making it useful for simple event tracking where filtering by topics is not needed.

Specification

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

Behavior

LOG0 pops offset and length from the stack, reads data from memory, and appends a log entry:
  • Offset: Starting position in memory (256-bit value)
  • Length: Number of bytes to read from memory (256-bit value)
  • Data: Bytes read from memory, padded with zeros if beyond allocated memory
  • Topics: Empty array (0 indexed parameters)

Memory Expansion

If the data range extends beyond current memory allocation, memory expands to word boundaries:
new_memory_size = (ceil((offset + length) / 32)) * 32

Static Call Protection

LOG0 cannot execute in static call context (EIP-214):
function badLog() external view {
    // Reverts: StaticCallViolation
}

Examples

Empty Log

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

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

const err = handler_0xa0_LOG0(frame);
console.log(err); // null (success)
console.log(frame.logs); // [{ address, topics: [], data: Uint8Array(0) }]
console.log(frame.gasRemaining); // 999625n (1000000 - 375)

Log with Data

const frame = createFrame({
  address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  memory: new Map([
    [0, 0xde], [1, 0xad], [2, 0xbe], [3, 0xef]
  ]),
  stack: [0n, 4n],  // offset=0, length=4
  gasRemaining: 1000000n,
});

handler_0xa0_LOG0(frame);

const log = frame.logs[0];
console.log(log.data); // Uint8Array(4) [0xde, 0xad, 0xbe, 0xef]
console.log(frame.gasRemaining); // 999617n (375 base + 32 memory + 32 data)

Solidity Event with No Topics

event SimpleLog(string message);

contract Logger {
  function log(string memory msg) public {
    emit SimpleLog(msg);  // Compiler generates LOG0
  }
}

// Usage
Logger logger = new Logger();
logger.log("Hello, world!");
// Transaction receipt includes log with empty topics

Non-Indexed Event Parameters

event Transfer(uint256 indexed id, address from, address to, uint256 value);

// If only 'id' is indexed, Solidity uses LOG1
// But we can emit an event with all non-indexed params using LOG0:

event Data(string text, uint256 amount, bytes payload);

contract DataLogger {
  function logData(string memory text, uint256 amount, bytes memory payload) public {
    emit Data(text, amount, payload);  // LOG0 (no indexed params)
  }
}

Gas Cost

Base Cost: 375 gas Data Cost: 8 gas per byte Memory Expansion: Proportional to new memory range Examples:
  • Empty: 375 gas
  • 1 byte: 375 + 8 = 383 gas
  • 32 bytes: 375 + 256 = 631 gas
  • 64 bytes: 375 + 512 + 3 (memory expansion) = 890 gas

Edge Cases

Zero-Length Log

const frame = createFrame({ stack: [100n, 0n] });
handler_0xa0_LOG0(frame);
// Valid: logs empty data, no memory expansion

Large Data

const frame = createFrame({
  stack: [0n, 10000n],
  gasRemaining: 100000n,
});
handler_0xa0_LOG0(frame);
// Gas: 375 + 80000 (data) + memory expansion
// Result: OutOfGas (insufficient gas)

Out of Bounds Memory

const frame = createFrame({
  stack: [0n, 1000n],
  memory: new Map(),  // Empty
  gasRemaining: 100000n,
});
handler_0xa0_LOG0(frame);
// Memory fills with zeros from current_size to 1000
// log.data = Uint8Array(1000) filled with zeros

Stack Underflow

const frame = createFrame({ stack: [0n] });  // Only 1 item
const err = handler_0xa0_LOG0(frame);
console.log(err); // { type: "StackUnderflow" }

Out of Gas

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

Common Usage

Simple State Changes

event Minted(uint256 amount);
event Burned(uint256 amount);

contract Token {
  function mint(uint256 amount) public {
    totalSupply += amount;
    emit Minted(amount);  // LOG0
  }

  function burn(uint256 amount) public {
    totalSupply -= amount;
    emit Burned(amount);  // LOG0
  }
}

Unindexed Data Logging

event ConfigUpdated(string newConfig);

contract Config {
  function updateConfig(string memory newConfig) public {
    config = newConfig;
    emit ConfigUpdated(newConfig);  // LOG0 (newConfig is non-indexed)
  }
}

Status Events

event StatusChanged(string status);

contract Service {
  function shutdown() public {
    isActive = false;
    emit StatusChanged("OFFLINE");  // LOG0
  }
}

Security

Cannot Block On-Chain Filtering

Since LOG0 has no topics, external systems cannot efficiently filter by indexed parameters. This is intentional—use LOG1-LOG4 when filtering capability is needed.
// INEFFICIENT: No topic filtering
event EventWithoutTopics(address user, uint256 amount);

// BETTER: Use indexed parameters
event EventWithTopics(address indexed user, uint256 amount);

Static Call Context Restrictions

LOG0 reverts in view/pure functions or during staticcall operations:
function badView() external view {
  emit SomeEvent();  // Reverts: cannot log in view context
}

contract Caller {
  function staticCallBad(address target) public {
    target.staticcall(abi.encodeCall(Logger.log, ()));  // Reverts
  }
}

Memory Boundaries

LOG0 reads memory up to offset + length. Uninitialized memory is zero-filled:
// Be aware of what's written to memory
function logUninitialized() public {
  // If no data written to memory[0:100], emits 100 zero bytes
  emit Data();
}

Implementation

/**
 * LOG0 opcode (0xa0) - Emit log with no indexed topics
 */
export function handler_0xa0_LOG0(frame: FrameType): EvmError | null {
  // Check static call (EIP-214)
  if (frame.isStatic) {
    return { type: "WriteProtection" };
  }

  // Check stack (need offset, length)
  if (frame.stack.length < 2) {
    return { type: "StackUnderflow" };
  }

  // Pop offset and length
  const offset = frame.stack.pop();
  const length = frame.stack.pop();

  // Validate bounds (u32 max)
  if (offset > Number.MAX_SAFE_INTEGER || length > Number.MAX_SAFE_INTEGER) {
    return { type: "OutOfBounds" };
  }

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

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

  // Memory expansion cost
  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);
  }

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

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

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

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

  // Increment PC
  frame.pc += 1;

  return null;
}

Testing

Test Cases

import { describe, it, expect } from 'vitest';
import { handler_0xa0_LOG0 } from './0xa0_LOG0.js';

describe('LOG0 (0xa0)', () => {
  it('emits log with empty data', () => {
    const frame = createFrame({
      stack: [0n, 0n],
      gasRemaining: 1000000n,
    });
    const err = handler_0xa0_LOG0(frame);
    expect(err).toBeNull();
    expect(frame.logs).toHaveLength(1);
    expect(frame.logs[0].topics).toEqual([]);
    expect(frame.gasRemaining).toBe(999625n);
  });

  it('reads data from memory', () => {
    const frame = createFrame({
      memory: new Map([[0, 0xde], [1, 0xad], [2, 0xbe], [3, 0xef]]),
      stack: [0n, 4n],
      gasRemaining: 1000000n,
    });
    handler_0xa0_LOG0(frame);
    const log = frame.logs[0];
    expect(log.data).toEqual(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
  });

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

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

  it('returns OutOfGas when insufficient gas', () => {
    const frame = createFrame({
      stack: [0n, 0n],
      gasRemaining: 374n,
    });
    const err = handler_0xa0_LOG0(frame);
    expect(err).toEqual({ type: "OutOfGas" });
  });

  it('expands memory correctly', () => {
    const frame = createFrame({
      stack: [0n, 100n],
      gasRemaining: 1000000n,
    });
    handler_0xa0_LOG0(frame);
    expect(frame.memorySize).toBe(128); // ceil(100/32)*32
  });
});

References