Skip to main content

Overview

Opcode: 0x14 Introduced: Frontier (EVM genesis) EQ performs bitwise equality comparison on two 256-bit values. Returns 1 if the values are exactly equal, 0 otherwise. This is a fundamental operation for implementing conditional logic, address validation, and state checks. Unlike comparison operations (LT/GT), EQ works identically for both signed and unsigned interpretations since it performs exact bitwise equality.

Specification

Stack Input:
a (top)
b
Stack Output:
a == b ? 1 : 0
Gas Cost: 3 (GasFastestStep) Operation:
result = (a == b) ? 1 : 0

Behavior

EQ pops two values from the stack, compares them bitwise, and pushes 1 if they are exactly equal, otherwise 0:
  • If a == b (all 256 bits identical): Result is 1 (true)
  • If a != b (any bit differs): Result is 0 (false)
Comparison is bitwise - no interpretation as signed/unsigned is needed.

Examples

Basic Equality

import { eq } from '@tevm/voltaire/evm/comparison';
import { createFrame } from '@tevm/voltaire/evm/Frame';

// 42 == 42 = 1 (true)
const frame = createFrame({ stack: [42n, 42n] });
const err = eq(frame);

console.log(frame.stack); // [1n]
console.log(frame.gasRemaining); // Original - 3

Inequality

// 10 == 20 = 0 (false)
const frame = createFrame({ stack: [10n, 20n] });
const err = eq(frame);

console.log(frame.stack); // [0n]

Zero Equality

// 0 == 0 = 1 (true)
const frame = createFrame({ stack: [0n, 0n] });
eq(frame);
console.log(frame.stack); // [1n]

// 0 == 1 = 0 (false)
const frame2 = createFrame({ stack: [0n, 1n] });
eq(frame2);
console.log(frame2.stack); // [0n]

Maximum Value

// MAX == MAX = 1 (true)
const MAX = (1n << 256n) - 1n;
const frame = createFrame({ stack: [MAX, MAX] });
eq(frame);

console.log(frame.stack); // [1n]

Address Comparison

// Common use case: checking addresses
const addr1 = 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb3n;
const addr2 = 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb3n;
const frame = createFrame({ stack: [addr1, addr2] });
eq(frame);

console.log(frame.stack); // [1n] (addresses match)

Sign-Independent

// EQ doesn't care about signed interpretation
const value1 = (1n << 256n) - 1n;  // All bits set
const value2 = (1n << 256n) - 1n;

// As unsigned: 2^256 - 1
// As signed: -1
// EQ: bitwise equal = 1
const frame = createFrame({ stack: [value1, value2] });
eq(frame);

console.log(frame.stack); // [1n]

Gas Cost

Cost: 3 gas (GasFastestStep) EQ shares the lowest gas tier with all comparison operations:
  • LT, GT, SLT, SGT, EQ (comparisons)
  • ISZERO, NOT
  • ADD, SUB
Comparison:
  • EQ: 3 gas
  • LT/GT: 3 gas
  • MUL/DIV: 5 gas

Edge Cases

Identical Values

// Any value equals itself
eq(createFrame({ stack: [0n, 0n] }));  // [1n]
eq(createFrame({ stack: [42n, 42n] }));  // [1n]

const MAX = (1n << 256n) - 1n;
eq(createFrame({ stack: [MAX, MAX] }));  // [1n]

Different Values

// Different values are never equal
eq(createFrame({ stack: [1n, 2n] }));  // [0n]
eq(createFrame({ stack: [0n, 1n] }));  // [0n]

const MAX = (1n << 256n) - 1n;
eq(createFrame({ stack: [MAX, MAX - 1n] }));  // [0n]

Large Values

// Arbitrary precision equality
const a = 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFn;
const b = 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFn;

const frame = createFrame({ stack: [a, b] });
eq(frame);
console.log(frame.stack); // [1n]

// One bit different
const c = 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEEn;
const frame2 = createFrame({ stack: [a, c] });
eq(frame2);
console.log(frame2.stack); // [0n]

Stack Underflow

// Not enough stack items
const frame = createFrame({ stack: [42n] });
const err = eq(frame);

console.log(err); // { type: "StackUnderflow" }
console.log(frame.stack); // [42n] (unchanged)

Out of Gas

// Insufficient gas
const frame = createFrame({ stack: [42n, 42n], gasRemaining: 2n });
const err = eq(frame);

console.log(err); // { type: "OutOfGas" }
console.log(frame.gasRemaining); // 0n

Common Usage

Address Validation

// require(addr != address(0))
assembly {
    if eq(addr, 0) {
        revert(0, 0)
    }
}

// Check specific address
assembly {
    if iszero(eq(caller(), expectedAddr)) {
        revert(0, 0)
    }
}

State Checks

// Check if storage value matches expected
assembly {
    let value := sload(slot)
    if iszero(eq(value, expected)) {
        revert(0, 0)
    }
}

Conditional Execution

// if (a == b)
assembly {
    let equal := eq(a, b)
    if equal {
        // Execute if equal
    }
}

Enum Comparison

// Check enum state
enum State { Pending, Active, Closed }

assembly {
    let state := sload(stateSlot)
    if eq(state, 1) {  // State.Active
        // Handle active state
    }
}

Hash Verification

// Verify hash matches
assembly {
    let computed := keccak256(dataPtr, dataLen)
    if iszero(eq(computed, expectedHash)) {
        revert(0, 0)
    }
}

Implementation

/**
 * EQ opcode (0x14) - Equality comparison
 */
export function handle(frame: FrameType): EvmError | null {
  // Consume gas (GasFastestStep = 3)
  const gasErr = consumeGas(frame, FastestStep);
  if (gasErr) return gasErr;

  // Pop operands (b is top, a is second)
  const bResult = popStack(frame);
  if (bResult.error) return bResult.error;
  const b = bResult.value;

  const aResult = popStack(frame);
  if (aResult.error) return aResult.error;
  const a = aResult.value;

  // Compare: a == b (bitwise equality)
  const result = a === b ? 1n : 0n;

  // Push result
  const pushErr = pushStack(frame, result);
  if (pushErr) return pushErr;

  // Increment PC
  frame.pc += 1;
  return null;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { handle as EQ } from './0x14_EQ.js';

describe('EQ (0x14)', () => {
  it('returns 1 when values are equal', () => {
    const frame = createFrame([42n, 42n]);
    expect(EQ(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
    expect(frame.pc).toBe(1);
    expect(frame.gasRemaining).toBe(997n);
  });

  it('returns 0 when values are not equal', () => {
    const frame = createFrame([10n, 20n]);
    expect(EQ(frame)).toBeNull();
    expect(frame.stack).toEqual([0n]);
  });

  it('handles zero equality', () => {
    const frame = createFrame([0n, 0n]);
    expect(EQ(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
  });

  it('handles zero inequality', () => {
    const frame = createFrame([0n, 1n]);
    expect(EQ(frame)).toBeNull();
    expect(frame.stack).toEqual([0n]);
  });

  it('handles max uint256 equality', () => {
    const MAX = (1n << 256n) - 1n;
    const frame = createFrame([MAX, MAX]);
    expect(EQ(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
  });

  it('handles large value comparison', () => {
    const val = 0xDEADBEEFn;
    const frame = createFrame([val, val]);
    expect(EQ(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
  });

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

  it('returns OutOfGas when insufficient gas', () => {
    const frame = createFrame([42n, 42n], 2n);
    expect(EQ(frame)).toEqual({ type: 'OutOfGas' });
  });

  it('preserves stack below compared values', () => {
    const frame = createFrame([100n, 200n, 42n, 42n]);
    expect(EQ(frame)).toBeNull();
    expect(frame.stack).toEqual([100n, 200n, 1n]);
  });
});

Edge Cases Tested

  • Equal values (42 == 42)
  • Unequal values (10 != 20)
  • Zero equality (0 == 0)
  • Zero inequality (0 != 1)
  • Maximum value equality
  • Large value comparison
  • Stack underflow (< 2 items)
  • Out of gas (< 3 gas)
  • Stack preservation

Security

Zero Address Checks

CRITICAL: Always validate addresses are not zero:
// VULNERABLE: Missing zero address check
function transfer(address to, uint256 amount) {
    balances[to] += amount;  // Can burn tokens to 0x0
}

// CORRECT: Explicit zero check
function transfer(address to, uint256 amount) {
    require(to != address(0), "zero address");
    balances[to] += amount;
}

// Assembly version
assembly {
    if eq(to, 0) {
        revert(0, 0)
    }
}

Access Control

// VULNERABLE: Missing ownership check
function withdraw() {
    // Anyone can call!
    payable(msg.sender).transfer(address(this).balance);
}

// CORRECT: Owner validation
function withdraw() {
    require(msg.sender == owner, "not owner");
    payable(msg.sender).transfer(address(this).balance);
}

// Assembly version
assembly {
    if iszero(eq(caller(), owner)) {
        revert(0, 0)
    }
}

State Validation

// VULNERABLE: Missing state check
function execute() {
    // Execute regardless of state
    doAction();
}

// CORRECT: State validation
enum State { Pending, Active, Closed }
State public state;

function execute() {
    require(state == State.Active, "not active");
    doAction();
}

Hash Comparison

// VULNERABLE: Using incorrect hash
bytes32 public secretHash;

function reveal(bytes32 secret) {
    // Wrong: comparing unhashed value to hash
    require(secret == secretHash);  // Never matches!
}

// CORRECT: Hash before comparison
function reveal(bytes32 secret) {
    require(keccak256(abi.encodePacked(secret)) == secretHash);
}

Optimizations

Commutative Property

// EQ is commutative: a == b is same as b == a
assembly {
    let equal := eq(a, b)
    // Same as:
    let equal := eq(b, a)
}

// Choose order to minimize stack operations

Zero Check Pattern

// Checking equality to zero
assembly {
    let isZero := eq(value, 0)  // 3 gas
}

// Equivalent using ISZERO (same gas, clearer intent)
assembly {
    let isZero := iszero(value)  // 3 gas
}

// Prefer ISZERO for zero checks (more readable)

Inverted Logic

// Check inequality: a != b
assembly {
    let notEqual := iszero(eq(a, b))  // 6 gas (EQ + ISZERO)
}

// Sometimes more efficient to structure logic around equality
assembly {
    if eq(a, b) {
        // Equal case
    } else {
        // Not equal case (no ISZERO needed)
    }
}

Multiple Comparisons

// Check if value equals any of multiple options
assembly {
    let match := or(
        eq(value, option1),
        or(eq(value, option2), eq(value, option3))
    )
}

// More efficient with structured checks
assembly {
    let match := 0
    if eq(value, option1) { match := 1 }
    if eq(value, option2) { match := 1 }
    if eq(value, option3) { match := 1 }
}

Benchmarks

EQ is one of the fastest EVM operations: Execution time (relative):
  • EQ: 1.0x (baseline)
  • LT/GT: 1.0x
  • ISZERO: 0.95x
  • ADD: 1.0x
  • MUL: 1.5x
Gas efficiency:
  • 3 gas per equality check
  • ~333,333 comparisons per million gas
  • Highly optimized in all EVM implementations

References

  • ISZERO - Zero check (specialized EQ)
  • LT - Less than (unsigned)
  • GT - Greater than (unsigned)
  • SLT - Signed less than
  • SGT - Signed greater than