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: 0x03 Introduced: Frontier (EVM genesis) SUB performs subtraction on two 256-bit unsigned integers with wrapping underflow semantics. When the result is negative (first operand < second operand), it wraps around modulo 2^256 to produce a large positive value. This operation is essential for decrements, difference calculations, and implementing signed arithmetic in smart contracts.

Specification

Stack Input:
a (top)
b
Stack Output:
(a - b) mod 2^256
Gas Cost: 3 (GasFastestStep) Operation:
result = (a - b) & ((1 << 256) - 1)

Behavior

SUB pops two values from the stack (a first, then b), computes a - b, and pushes the result. Underflow wraps around without exceptions:
  • If a >= b: Result is the mathematical difference
  • If a < b: Result wraps to 2^256 - (b - a)
No exceptions are thrown for underflow. The result always fits in 256 bits.

Examples

Basic Subtraction

import { sub } from '@tevm/voltaire/evm/arithmetic';
import { createFrame } from '@tevm/voltaire/evm/Frame';

// 10 - 5 = 5
const frame = createFrame({ stack: [10n, 5n] });
const err = sub(frame);

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

Underflow Wrapping

// 0 - 1 wraps to maximum value
const frame = createFrame({ stack: [0n, 1n] });
const err = sub(frame);

const MAX = (1n << 256n) - 1n;
console.log(frame.stack); // [MAX]

Large Underflow

// 5 - 10 wraps around
const frame = createFrame({ stack: [5n, 10n] });
const err = sub(frame);

// Result: 2^256 - 5 = MAX - 4
const MAX = (1n << 256n) - 1n;
console.log(frame.stack); // [MAX - 4n]

Identity Element

// Subtracting zero
const frame = createFrame({ stack: [42n, 0n] });
const err = sub(frame);

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

Self-Subtraction

// x - x = 0
const frame = createFrame({ stack: [42n, 42n] });
const err = sub(frame);

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

Gas Cost

Cost: 3 gas (GasFastestStep) SUB shares the lowest gas tier with ADD, making it one of the cheapest operations: Comparison:
  • ADD/SUB: 3 gas
  • MUL/DIV/MOD: 5 gas
  • ADDMOD/MULMOD: 8 gas
  • EXP: 10 + 50 per byte
SUB and ADD have identical gas costs due to similar computational complexity in hardware.

Edge Cases

Maximum Underflow

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

console.log(frame.stack); // [1n]
// Because: (0 - MAX) mod 2^256 = 1

Zero Subtraction

// 0 - 0 = 0
const frame = createFrame({ stack: [0n, 0n] });
sub(frame);

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

Operand Order Matters

// SUB is NOT commutative: a - b ≠ b - a
const frame1 = createFrame({ stack: [10n, 5n] });
sub(frame1);  // 10 - 5 = 5

const frame2 = createFrame({ stack: [5n, 10n] });
sub(frame2);  // 5 - 10 = wraps

console.log(frame1.stack[0] === frame2.stack[0]); // false

Stack Underflow

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

console.log(err); // { type: "StackUnderflow" }

Common Usage

Balance Updates

// Decrease balance
function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount, "insufficient balance");
    balances[msg.sender] -= amount;  // SUB opcode
    payable(msg.sender).transfer(amount);
}

Loop Counters (Decrement)

// Countdown loop
for (uint i = n; i > 0; i--) {
    // Compiler generates: SUB i, 1
}

Difference Calculations

// Time elapsed
function elapsed(uint256 startTime) view returns (uint256) {
    return block.timestamp - startTime;
}

// Price difference
function priceGap(uint256 buyPrice, uint256 sellPrice)
    pure returns (uint256) {
    require(sellPrice >= buyPrice, "invalid prices");
    return sellPrice - buyPrice;
}

Range Checks

// Check if value is in range [min, max]
function inRange(uint256 value, uint256 min, uint256 max)
    pure returns (bool) {
    return (value - min) <= (max - min);
    // Uses wrapping: if value < min, underflows to large number
}

Safe vs Unchecked

Solidity 0.8.0+:
// Default: checked arithmetic (adds underflow checks)
uint256 result = a - b;  // Reverts on underflow

// Explicit wrapping (uses raw SUB)
unchecked {
    uint256 result = a - b;  // Wraps on underflow
}

Implementation

/**
 * SUB opcode (0x03) - Subtraction with underflow wrapping
 */
export function sub(frame: FrameType): EvmError | null {
  // Consume gas (GasFastestStep = 3)
  frame.gasRemaining -= 3n;
  if (frame.gasRemaining < 0n) {
    frame.gasRemaining = 0n;
    return { type: "OutOfGas" };
  }

  // Pop operands (a - b)
  if (frame.stack.length < 2) return { type: "StackUnderflow" };
  const a = frame.stack.pop();  // top
  const b = frame.stack.pop();  // second

  // Compute result with wrapping (modulo 2^256)
  const result = (a - b) & ((1n << 256n) - 1n);

  // Push result
  if (frame.stack.length >= 1024) return { type: "StackOverflow" };
  frame.stack.push(result);

  // Increment PC
  frame.pc += 1;

  return null;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { sub } from './0x03_SUB.js';

describe('SUB (0x03)', () => {
  it('subtracts two numbers', () => {
    const frame = createFrame([10n, 5n]);
    expect(sub(frame)).toBeNull();
    expect(frame.stack).toEqual([5n]);
  });

  it('handles underflow wrapping', () => {
    const frame = createFrame([0n, 1n]);
    expect(sub(frame)).toBeNull();
    const MAX = (1n << 256n) - 1n;
    expect(frame.stack).toEqual([MAX]);
  });

  it('handles large underflow', () => {
    const frame = createFrame([5n, 10n]);
    expect(sub(frame)).toBeNull();
    const MAX = (1n << 256n) - 1n;
    expect(frame.stack).toEqual([MAX - 4n]);
  });

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

  it('handles self-subtraction', () => {
    const frame = createFrame([42n, 42n]);
    expect(sub(frame)).toBeNull();
    expect(frame.stack).toEqual([0n]);
  });

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

  it('consumes correct gas (3)', () => {
    const frame = createFrame([10n, 5n], 100n);
    expect(sub(frame)).toBeNull();
    expect(frame.gasRemaining).toBe(97n);
  });
});

Security

Underflow Vulnerabilities

Classic vulnerability (pre-0.8.0):
// VULNERABLE: No underflow protection
function withdraw(uint256 amount) public {
    balances[msg.sender] -= amount;  // Can underflow!
    payable(msg.sender).transfer(amount);
}
Attack scenario:
// User with balance 5 withdraws 10
// balances[msg.sender] = 5 - 10 = wraps to MAX_UINT256
// Attacker now has infinite balance
Famous exploit: BatchOverflow (2018)
// Beauty Chain (BEC) token vulnerability
function batchTransfer(address[] recipients, uint256 value) {
    uint256 amount = recipients.length * value;  // Can overflow!
    require(balances[msg.sender] >= amount);    // Check bypassed

    balances[msg.sender] -= amount;  // Underflow if amount wrapped
    for (uint i = 0; i < recipients.length; i++) {
        balances[recipients[i]] += value;
    }
}

// Attack: batchTransfer([addr1, addr2], 2^255)
// amount = 2 * 2^255 = wraps to 0
// Check passes, sender balance unchanged, recipients get tokens

Safe Patterns (Pre-0.8.0)

// SafeMath library
function safeSub(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b <= a, "subtraction underflow");
    return a - b;
}

function withdraw(uint256 amount) public {
    balances[msg.sender] = safeSub(balances[msg.sender], amount);
    payable(msg.sender).transfer(amount);
}

Modern Solidity (0.8.0+)

// Automatic underflow checks
function withdraw(uint256 amount) public {
    balances[msg.sender] -= amount;  // Reverts on underflow
    payable(msg.sender).transfer(amount);
}

// Explicit wrapping when needed
function decrementWrapping(uint256 counter) pure returns (uint256) {
    unchecked {
        return counter - 1;  // Uses raw SUB, wraps on underflow
    }
}

Comparison Patterns

// Check difference without underflow risk
function isGreater(uint256 a, uint256 b) pure returns (bool) {
    // WRONG: Can underflow
    // return (a - b) > 0;

    // RIGHT: Direct comparison
    return a > b;
}

// Safe difference with minimum value
function safeDifference(uint256 a, uint256 b) pure returns (uint256) {
    return a > b ? a - b : 0;
}

Benchmarks

SUB performance characteristics: Execution time (relative):
  • SUB: 1.0x (same as ADD)
  • MUL: 1.2x
  • DIV: 2.5x
Gas efficiency:
  • 3 gas per 256-bit subtraction
  • ~333,333 subtractions per million gas
  • Identical cost to ADD
Optimization:
// These have identical gas costs:
uint256 result1 = a - b;  // 3 gas
uint256 result2 = a + (~b + 1);  // More expensive (NOT + ADD + ADD)

// Prefer SUB for clarity and efficiency

References