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: 0x10 Introduced: Frontier (EVM genesis) LT performs unsigned less than comparison on two 256-bit integers. Returns 1 if the first value is strictly less than the second, 0 otherwise. All values are treated as unsigned integers in the range 0 to 2^256 - 1. This is the fundamental comparison operation for implementing conditional logic and bounds checking in smart contracts.

Specification

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

Behavior

LT pops two values from the stack, compares them as unsigned 256-bit integers, and pushes 1 if a < b, otherwise 0:
  • If a < b: Result is 1 (true)
  • If a >= b: Result is 0 (false)
All comparisons are unsigned. Values with bit 255 set are treated as large positive numbers, not negative values.

Examples

Basic Comparison

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

// 5 < 10 = 1 (true)
const frame = createFrame({ stack: [5n, 10n] });
const err = lt(frame);

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

Equal Values

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

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

Greater Value

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

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

Zero Comparison

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

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

Maximum Values

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

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

Unsigned Treatment

// 2^255 is treated as large positive (not negative)
const SIGN_BIT = 1n << 255n;

// 1 < 2^255 = 1 (true, unsigned comparison)
const frame = createFrame({ stack: [1n, SIGN_BIT] });
lt(frame);

console.log(frame.stack); // [1n]
// In signed comparison (SLT), this would be 0 because 2^255 = -2^255 (negative)

Gas Cost

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

Edge Cases

Boundary Values

const MAX = (1n << 256n) - 1n;

// 0 < MAX = 1
lt(createFrame({ stack: [0n, MAX] }));  // [1n]

// MAX < 0 = 0
lt(createFrame({ stack: [MAX, 0n] }));  // [0n]

// MAX < MAX = 0
lt(createFrame({ stack: [MAX, MAX] }));  // [0n]

Sign Bit Set

// Values with bit 255 set are large positive (unsigned)
const SIGN_BIT = 1n << 255n;  // 2^255

// SIGN_BIT is treated as 2^255, not -2^255
// 2^255 < 1 = 0 (false, unsigned)
lt(createFrame({ stack: [SIGN_BIT, 1n] }));  // [0n]

// Compare with SLT (signed):
// SLT would return 1 because 2^255 = -2^255 < 1 (signed)

Stack Underflow

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

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

Out of Gas

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

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

Large Values

// Arbitrary precision supported
const a = 123456789012345678901234567890n;
const b = 987654321098765432109876543210n;

const frame = createFrame({ stack: [a, b] });
lt(frame);

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

Common Usage

Bounds Checking

// require(index < length)
assembly {
    if iszero(lt(index, length)) {
        revert(0, 0)
    }
}

Range Validation

// Check if value < max
assembly {
    let valid := lt(value, max)
    if iszero(valid) {
        revert(0, 0)
    }
}

Loop Conditions

// for (uint i = 0; i < n; i++)
assembly {
    let i := 0
    for {} lt(i, n) { i := add(i, 1) } {
        // Loop body
    }
}

Minimum Value

// min(a, b)
assembly {
    let minimum := a
    if lt(b, a) {
        minimum := b
    }
}

Array Access Safety

// Safe array access
assembly {
    if lt(index, arrLength) {
        let value := sload(add(arrSlot, index))
        // Use value
    }
}

Implementation

/**
 * LT opcode (0x10) - Less than comparison (unsigned)
 */
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 (unsigned)
  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 LT } from './0x10_LT.js';

describe('LT (0x10)', () => {
  it('returns 1 when a < b', () => {
    const frame = createFrame([10n, 20n]);
    expect(LT(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
    expect(frame.pc).toBe(1);
    expect(frame.gasRemaining).toBe(997n);
  });

  it('returns 0 when a >= b (equal)', () => {
    const frame = createFrame([20n, 20n]);
    expect(LT(frame)).toBeNull();
    expect(frame.stack).toEqual([0n]);
  });

  it('returns 0 when a > b', () => {
    const frame = createFrame([30n, 20n]);
    expect(LT(frame)).toBeNull();
    expect(frame.stack).toEqual([0n]);
  });

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

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

  it('treats all values as unsigned', () => {
    // 2^255 is large positive as unsigned
    const SIGN_BIT = 1n << 255n;
    const frame = createFrame([1n, SIGN_BIT]);
    expect(LT(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]); // 1 < 2^255
  });

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

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

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

Edge Cases Tested

  • Basic comparisons (a < b, a = b, a > b)
  • Zero comparisons (0 < 1, 1 < 0)
  • Maximum values (MAX-1 < MAX)
  • Unsigned treatment (sign bit set)
  • Stack underflow (< 2 items)
  • Out of gas (< 3 gas)
  • Large arbitrary values
  • Stack preservation

Security

Unsigned vs Signed Confusion

CRITICAL: LT treats all values as unsigned. Do not use for signed integer comparisons:
// VULNERABLE: Using LT for signed values
function withdraw(int256 amount) {
    // LT treats -1 as 2^256-1 (huge positive!)
    assembly {
        if lt(balance, amount) {  // WRONG!
            revert(0, 0)
        }
    }
    // Negative amounts bypass the check
}

// CORRECT: Use SLT for signed comparisons
function withdraw(int256 amount) {
    assembly {
        if slt(balance, amount) {  // Correct
            revert(0, 0)
        }
    }
}

Off-by-One Errors

// VULNERABLE: Wrong boundary
require(index <= array.length);  // Allows out-of-bounds!

// CORRECT: Strict less than
require(index < array.length);  // Max valid: length - 1

Integer Overflow Before Comparison

// VULNERABLE: Overflow corrupts comparison
uint256 sum = a + b;  // May wrap to small value
require(sum > a);     // Check may incorrectly pass

// CORRECT: Check before operation
require(a <= type(uint256).max - b);
uint256 sum = a + b;

Type Width Issues

// VULNERABLE: Comparing different widths
uint256 large = type(uint256).max;
uint128 small = type(uint128).max;

// Implicit cast may truncate
require(large < small);  // Type confusion

// CORRECT: Explicit same-width comparison
require(uint256(large) < uint256(small));

Optimizations

Inversion Patterns

// These are equivalent:
// a < b  ===  !(a >= b)

assembly {
    // Direct
    let less := lt(a, b)

    // Inverted (sometimes useful in complex conditions)
    let less := iszero(or(gt(a, b), eq(a, b)))
}

Short-Circuit Evaluation

// Evaluate cheapest condition first
assembly {
    if lt(index, length) {
        // Only check expensive condition if first passes
        if expensiveCheck() {
            // Execute
        }
    }
}

Constant Comparison

// Compiler may optimize constant comparisons
assembly {
    if lt(value, 100) {  // Constant 100
        // Optimized by EVM implementations
    }
}

Benchmarks

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

References

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