Skip to main content

Overview

Opcode: 0x13 Introduced: Frontier (EVM genesis) SGT performs signed greater than comparison on two 256-bit integers interpreted as two’s complement signed values. Returns 1 if the first value is strictly greater than the second, 0 otherwise. Values are in the range -2^255 to 2^255 - 1. This operation complements SLT for implementing signed conditional logic and range checks.

Specification

Stack Input:
a (top)
b
Stack Output:
signed(a) > signed(b) ? 1 : 0
Gas Cost: 3 (GasFastestStep) Operation:
// Interpret as signed two's complement
signed_a = a >= 2^255 ? a - 2^256 : a
signed_b = b >= 2^255 ? b - 2^256 : b
result = (signed_a > signed_b) ? 1 : 0

Behavior

SGT pops two values from the stack, interprets them as signed 256-bit two’s complement integers, compares them, and pushes 1 if signed(a) > signed(b), otherwise 0:
  • If signed(a) > signed(b): Result is 1 (true)
  • If signed(a) <= signed(b): Result is 0 (false)
Two’s complement interpretation:
  • Bit 255 = 0: Positive (0 to 2^255 - 1)
  • Bit 255 = 1: Negative (-2^255 to -1)

Examples

Positive Values

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

// 20 > 10 = 1 (both positive)
const frame = createFrame({ stack: [20n, 10n] });
const err = sgt(frame);

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

Positive Greater Than Negative

// 10 > -1 = 1 (true)
const NEG_1 = (1n << 256n) - 1n;  // Two's complement -1
const frame = createFrame({ stack: [10n, NEG_1] });
sgt(frame);

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

Negative Less Than Positive

// -1 > 10 = 0 (false, -1 < 10)
const NEG_1 = (1n << 256n) - 1n;
const frame = createFrame({ stack: [NEG_1, 10n] });
sgt(frame);

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

Negative Value Comparison

// -5 > -10 = 1 (true)
const NEG_5 = (1n << 256n) - 5n;
const NEG_10 = (1n << 256n) - 10n;
const frame = createFrame({ stack: [NEG_5, NEG_10] });
sgt(frame);

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

Zero Boundary

// 0 > -1 = 1 (true)
const NEG_1 = (1n << 256n) - 1n;
const frame = createFrame({ stack: [0n, NEG_1] });
sgt(frame);

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

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

Minimum and Maximum

// MAX_INT256 > MIN_INT256 = 1
const MIN_INT256 = 1n << 255n;  // -2^255
const MAX_INT256 = (1n << 255n) - 1n;  // 2^255 - 1
const frame = createFrame({ stack: [MAX_INT256, MIN_INT256] });
sgt(frame);

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

Contrast with Unsigned GT

// 2^255 has bit 255 set
const SIGN_BIT = 1n << 255n;

// SGT: -2^255 > 1 = 0 (false, signed)
const frame1 = createFrame({ stack: [SIGN_BIT, 1n] });
sgt(frame1);
console.log(frame1.stack); // [0n]

// GT: 2^255 > 1 = 1 (true, unsigned - 2^255 is huge positive)
const frame2 = createFrame({ stack: [SIGN_BIT, 1n] });
gt(frame2);
console.log(frame2.stack); // [1n]

Gas Cost

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

Edge Cases

Signed Boundary Values

const MIN_INT256 = 1n << 255n;  // -2^255
const MAX_INT256 = (1n << 255n) - 1n;  // 2^255 - 1
const NEG_1 = (1n << 256n) - 1n;  // -1

// MAX > MIN
sgt(createFrame({ stack: [MAX_INT256, MIN_INT256] }));  // [1n]

// MIN < MAX
sgt(createFrame({ stack: [MIN_INT256, MAX_INT256] }));  // [0n]

// 0 > -1
sgt(createFrame({ stack: [0n, NEG_1] }));  // [1n]

// -1 < 0
sgt(createFrame({ stack: [NEG_1, 0n] }));  // [0n]

Equal Values

// Any value compared to itself
const NEG_10 = (1n << 256n) - 10n;

sgt(createFrame({ stack: [20n, 20n] }));  // [0n]
sgt(createFrame({ stack: [NEG_10, NEG_10] }));  // [0n]
sgt(createFrame({ stack: [0n, 0n] }));  // [0n]

Sign Bit Boundary

// Just below sign bit (largest positive)
const MAX_POS = (1n << 255n) - 1n;

// Just at sign bit (smallest negative)
const MIN_NEG = 1n << 255n;

// Unsigned: MIN_NEG > MAX_POS
gt(createFrame({ stack: [MIN_NEG, MAX_POS] }));  // [1n]

// Signed: MIN_NEG < MAX_POS
sgt(createFrame({ stack: [MIN_NEG, MAX_POS] }));  // [0n]

Stack Underflow

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

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

Out of Gas

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

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

Common Usage

Positive Value Check

// Check if value is positive (> 0)
assembly {
    let isPositive := sgt(value, 0)
    if iszero(isPositive) {
        revert(0, 0)
    }
}

Signed Upper Bounds

// require(signedValue <= max)  ===  require(!(signedValue > max))
assembly {
    if sgt(signedValue, max) {
        revert(0, 0)
    }
}

Maximum of Signed Values

// max(a, b) for signed integers
assembly {
    let maximum := a
    if sgt(b, a) {
        maximum := b
    }
}

Signed Range Validation

// Check if value in signed range (min, max)
assembly {
    let inRange := and(
        sgt(value, min),      // value > min
        iszero(sgt(value, max))  // value <= max
    )
}

Non-Negative Check

// require(value >= 0)  ===  require(!(value < 0))
assembly {
    if slt(value, 0) {
        revert(0, 0)
    }
}
// Equivalent:
assembly {
    if iszero(or(sgt(value, 0), iszero(value))) {
        revert(0, 0)
    }
}

Implementation

/**
 * SGT opcode (0x13) - Signed greater than 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;

  // Convert to signed and compare
  const aSigned = toSigned256(a);
  const bSigned = toSigned256(b);
  const result = aSigned > bSigned ? 1n : 0n;

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

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

/**
 * Convert unsigned 256-bit to signed two's complement
 */
function toSigned256(value: bigint): bigint {
  const MAX_INT256 = 1n << 255n;
  if (value >= MAX_INT256) {
    return value - (1n << 256n);
  }
  return value;
}

Testing

Test Coverage

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

describe('SGT (0x13)', () => {
  it('returns 1 when a > b (both positive)', () => {
    const frame = createFrame([30n, 20n]);
    expect(SGT(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
    expect(frame.gasRemaining).toBe(997n);
  });

  it('returns 1 when positive > negative', () => {
    const NEG_1 = (1n << 256n) - 1n;
    const frame = createFrame([10n, NEG_1]);
    expect(SGT(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]); // 10 > -1
  });

  it('returns 0 when negative < positive', () => {
    const NEG_1 = (1n << 256n) - 1n;
    const frame = createFrame([NEG_1, 10n]);
    expect(SGT(frame)).toBeNull();
    expect(frame.stack).toEqual([0n]); // -1 < 10
  });

  it('compares negative numbers correctly', () => {
    const NEG_5 = (1n << 256n) - 5n;
    const NEG_10 = (1n << 256n) - 10n;
    const frame = createFrame([NEG_5, NEG_10]);
    expect(SGT(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]); // -5 > -10
  });

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

  it('handles MAX_INT256 > MIN_INT256', () => {
    const MIN = 1n << 255n;
    const MAX = (1n << 255n) - 1n;
    const frame = createFrame([MAX, MIN]);
    expect(SGT(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
  });

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

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

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

Edge Cases Tested

  • Positive value comparisons
  • Positive greater than negative
  • Negative less than positive
  • Negative value comparisons (-5 > -10)
  • Zero boundary (0 > -1, 1 > 0)
  • MAX_INT256 and MIN_INT256
  • Equal values
  • Stack underflow
  • Out of gas
  • Stack preservation

Security

Critical: Signed vs Unsigned Confusion

COMMON VULNERABILITY: Using GT instead of SGT for signed values:
// VULNERABLE: Using GT for signed comparison
function isPositive(int256 value) returns (bool) {
    // GT treats -1 as 2^256-1 (huge positive!)
    assembly {
        return(0, gt(value, 0))  // WRONG!
    }
    // Returns true for negative values!
}

// CORRECT: Use SGT for signed values
function isPositive(int256 value) returns (bool) {
    assembly {
        return(0, sgt(value, 0))  // Correct
    }
}

Type Safety Issues

// VULNERABLE: Mixed signed/unsigned
function checkLimit(uint256 unsigned, int256 signed) {
    // Direct comparison uses unsigned semantics
    require(unsigned > signed);  // Type confusion!
}

// CORRECT: Explicit type handling
function checkLimit(uint256 unsigned, int256 signed) {
    require(signed >= 0, "negative value");
    require(unsigned > uint256(signed));
}

Overflow in Signed Operations

// VULNERABLE: Overflow before comparison
int256 result = a - b;  // May overflow
require(result > 0);    // Check may be wrong

// CORRECT: Check before operation
if (a > 0 && b < 0) {
    require(a <= type(int256).max + b, "overflow");
}
int256 result = a - b;

Sign Extension Errors

// VULNERABLE: Wrong sign extension
function extend(int8 small) returns (int256) {
    // Casting through uint loses sign
    return int256(uint256(uint8(small)));  // Wrong!
}

// CORRECT: Direct sign extension
function extend(int8 small) returns (int256) {
    return int256(small);  // Preserves sign
}

Optimizations

Relationship to SLT

// These are equivalent:
// a > b  ===  b < a

assembly {
    let greater := sgt(a, b)
    // Same as:
    let greater := slt(b, a)
}

// Choose based on stack layout to minimize swaps

Positive Check Optimization

// Check if value > 0
assembly {
    let isPos := sgt(value, 0)  // 3 gas
}

// Equivalent but more expensive:
assembly {
    let notNeg := iszero(slt(value, 0))  // 6 gas
    let notZero := iszero(iszero(value)) // 6 gas
    let isPos := and(notNeg, notZero)    // 9 gas total
}

Inversion Pattern

// Direct comparison (preferred)
assembly {
    let greater := sgt(a, b)  // 3 gas
}

// Inverted (avoid - more expensive)
assembly {
    let greater := iszero(or(slt(a, b), eq(a, b)))  // 12 gas
}

Benchmarks

SGT performance matches other comparison operations: Execution time (relative):
  • SGT: 1.05x (slightly slower due to sign conversion)
  • SLT: 1.05x
  • GT/LT/EQ: 1.0x
  • ISZERO: 0.95x
Gas efficiency:
  • 3 gas per signed comparison
  • ~333,333 comparisons per million gas
  • Sign conversion adds negligible overhead

References

  • SLT - Signed less than
  • GT - Unsigned greater than
  • LT - Unsigned less than
  • SDIV - Signed division
  • SMOD - Signed modulo