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: 0x12
Introduced: Frontier (EVM genesis)
SLT performs signed less than comparison on two 256-bit integers interpreted as two’s complement signed values. Returns 1 if the first value is strictly less than the second, 0 otherwise. Values are in the range -2^255 to 2^255 - 1.
This operation is critical for signed integer arithmetic and conditions involving negative values.
Specification
Stack Input:
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
SLT 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 { slt } from '@tevm/voltaire/evm/comparison';
import { createFrame } from '@tevm/voltaire/evm/Frame';
// 10 < 20 = 1 (both positive)
const frame = createFrame({ stack: [10n, 20n] });
const err = slt(frame);
console.log(frame.stack); // [1n]
console.log(frame.gasRemaining); // Original - 3
Negative Less Than Positive
// -1 < 10 = 1 (true)
const NEG_1 = (1n << 256n) - 1n; // Two's complement -1
const frame = createFrame({ stack: [NEG_1, 10n] });
slt(frame);
console.log(frame.stack); // [1n]
Positive Greater Than Negative
// 10 < -1 = 0 (false, 10 > -1)
const NEG_1 = (1n << 256n) - 1n;
const frame = createFrame({ stack: [10n, NEG_1] });
slt(frame);
console.log(frame.stack); // [0n]
Negative Value Comparison
// -10 < -5 = 1 (true)
const NEG_10 = (1n << 256n) - 10n;
const NEG_5 = (1n << 256n) - 5n;
const frame = createFrame({ stack: [NEG_10, NEG_5] });
slt(frame);
console.log(frame.stack); // [1n]
Zero Boundary
// -1 < 0 = 1 (true)
const NEG_1 = (1n << 256n) - 1n;
const frame = createFrame({ stack: [NEG_1, 0n] });
slt(frame);
console.log(frame.stack); // [1n]
// 0 < 1 = 1 (true)
const frame2 = createFrame({ stack: [0n, 1n] });
slt(frame2);
console.log(frame2.stack); // [1n]
Minimum and Maximum
// MIN_INT256 < MAX_INT256 = 1
const MIN_INT256 = 1n << 255n; // -2^255
const MAX_INT256 = (1n << 255n) - 1n; // 2^255 - 1
const frame = createFrame({ stack: [MIN_INT256, MAX_INT256] });
slt(frame);
console.log(frame.stack); // [1n]
Contrast with Unsigned LT
// 2^255 has bit 255 set
const SIGN_BIT = 1n << 255n;
// SLT: -2^255 < 1 = 1 (true, signed)
const frame1 = createFrame({ stack: [SIGN_BIT, 1n] });
slt(frame1);
console.log(frame1.stack); // [1n]
// LT: 2^255 < 1 = 0 (false, unsigned - 2^255 is huge positive)
const frame2 = createFrame({ stack: [SIGN_BIT, 1n] });
lt(frame2);
console.log(frame2.stack); // [0n]
Gas Cost
Cost: 3 gas (GasFastestStep)
SLT shares the lowest gas tier with all comparison operations:
- LT, GT, SLT, SGT, EQ (comparisons)
- ISZERO, NOT
- ADD, SUB
Comparison:
- SLT/SGT/LT/GT: 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
// MIN < MAX
slt(createFrame({ stack: [MIN_INT256, MAX_INT256] })); // [1n]
// MAX > MIN
slt(createFrame({ stack: [MAX_INT256, MIN_INT256] })); // [0n]
// -1 < 0
slt(createFrame({ stack: [NEG_1, 0n] })); // [1n]
// 0 > -1
slt(createFrame({ stack: [0n, NEG_1] })); // [0n]
Equal Values
// Any value compared to itself
const NEG_10 = (1n << 256n) - 10n;
slt(createFrame({ stack: [20n, 20n] })); // [0n]
slt(createFrame({ stack: [NEG_10, NEG_10] })); // [0n]
slt(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
lt(createFrame({ stack: [MIN_NEG, MAX_POS] })); // [0n]
// Signed: MIN_NEG < MAX_POS
slt(createFrame({ stack: [MIN_NEG, MAX_POS] })); // [1n]
Stack Underflow
// Not enough stack items
const frame = createFrame({ stack: [10n] });
const err = slt(frame);
console.log(err); // { type: "StackUnderflow" }
console.log(frame.stack); // [10n] (unchanged)
Out of Gas
// Insufficient gas
const frame = createFrame({ stack: [10n, 20n], gasRemaining: 2n });
const err = slt(frame);
console.log(err); // { type: "OutOfGas" }
console.log(frame.gasRemaining); // 0n
Common Usage
Signed Bounds Checking
// require(signedValue < max)
assembly {
if iszero(slt(signedValue, max)) {
revert(0, 0)
}
}
Negative Value Check
// Check if value is negative
assembly {
let isNegative := slt(value, 0)
if isNegative {
revert(0, 0)
}
}
Signed Range Validation
// Check if value in signed range [min, max]
assembly {
let inRange := and(
iszero(slt(value, min)), // value >= min
iszero(sgt(value, max)) // value <= max
)
}
Absolute Value
// abs(value)
assembly {
let abs := value
if slt(value, 0) {
abs := sub(0, value) // Negate
}
}
Sign Function
// sign(value): -1, 0, or 1
assembly {
let s := 0
if slt(value, 0) {
s := sub(0, 1) // -1
}
if sgt(value, 0) {
s := 1
}
}
Implementation
/**
* SLT opcode (0x12) - Signed less 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 SLT } from './0x12_SLT.js';
describe('SLT (0x12)', () => {
it('returns 1 when a < b (both positive)', () => {
const frame = createFrame([10n, 20n]);
expect(SLT(frame)).toBeNull();
expect(frame.stack).toEqual([1n]);
expect(frame.gasRemaining).toBe(997n);
});
it('returns 1 when negative < positive', () => {
const NEG_1 = (1n << 256n) - 1n;
const frame = createFrame([NEG_1, 10n]);
expect(SLT(frame)).toBeNull();
expect(frame.stack).toEqual([1n]); // -1 < 10
});
it('returns 0 when positive > negative', () => {
const NEG_1 = (1n << 256n) - 1n;
const frame = createFrame([10n, NEG_1]);
expect(SLT(frame)).toBeNull();
expect(frame.stack).toEqual([0n]); // 10 > -1
});
it('compares negative numbers correctly', () => {
const NEG_10 = (1n << 256n) - 10n;
const NEG_5 = (1n << 256n) - 5n;
const frame = createFrame([NEG_10, NEG_5]);
expect(SLT(frame)).toBeNull();
expect(frame.stack).toEqual([1n]); // -10 < -5
});
it('handles -1 < 0', () => {
const NEG_1 = (1n << 256n) - 1n;
const frame = createFrame([NEG_1, 0n]);
expect(SLT(frame)).toBeNull();
expect(frame.stack).toEqual([1n]);
});
it('handles MIN_INT256 < MAX_INT256', () => {
const MIN = 1n << 255n;
const MAX = (1n << 255n) - 1n;
const frame = createFrame([MIN, MAX]);
expect(SLT(frame)).toBeNull();
expect(frame.stack).toEqual([1n]);
});
it('returns 0 when a >= b (equal)', () => {
const frame = createFrame([20n, 20n]);
expect(SLT(frame)).toBeNull();
expect(frame.stack).toEqual([0n]);
});
it('returns StackUnderflow with insufficient stack', () => {
const frame = createFrame([10n]);
expect(SLT(frame)).toEqual({ type: 'StackUnderflow' });
});
it('preserves stack below compared values', () => {
const frame = createFrame([100n, 200n, 10n, 20n]);
expect(SLT(frame)).toBeNull();
expect(frame.stack).toEqual([100n, 200n, 1n]);
});
});
Edge Cases Tested
- Positive value comparisons
- Negative less than positive
- Positive greater than negative
- Negative value comparisons (-10 < -5)
- Zero boundary (-1 < 0, 0 < 1)
- MIN_INT256 and MAX_INT256
- Equal values
- Stack underflow
- Out of gas
- Stack preservation
Security
Critical: Signed vs Unsigned Confusion
MOST COMMON VULNERABILITY: Using LT instead of SLT for signed values:
// VULNERABLE: Using LT for signed comparison
function withdraw(int256 amount) {
// LT treats -1 as 2^256-1 (huge positive!)
assembly {
if lt(balance, amount) { // WRONG!
revert(0, 0)
}
}
// Attacker can pass negative amount to bypass check
}
// CORRECT: Use SLT for signed values
function withdraw(int256 amount) {
assembly {
if slt(balance, amount) { // Correct
revert(0, 0)
}
}
}
Integer Type Casting
// VULNERABLE: Unsafe cast before comparison
function compareValues(uint256 a, int256 b) {
// Casting signed to unsigned loses sign information
uint256 b_unsigned = uint256(b); // -1 becomes 2^256-1
require(a < b_unsigned); // Wrong comparison!
}
// CORRECT: Keep signed types consistent
function compareValues(int256 a, int256 b) {
require(a < b); // Compiler uses SLT
}
Overflow in Signed Arithmetic
// VULNERABLE: Overflow before comparison
int256 sum = a + b; // May overflow
require(sum > a); // Check may be wrong
// CORRECT: Check before operation
require(a > 0 && b > type(int256).max - a, "overflow");
int256 sum = a + b;
Sign Extension Issues
// VULNERABLE: Incorrect sign extension
int8 small = -1;
int256 large = int256(uint256(uint8(small))); // Wrong! Becomes 255
// CORRECT: Proper sign extension
int256 large = int256(small); // Correctly -1
Optimizations
Two’s Complement Implementation
The implementation efficiently converts to signed for comparison:
// Efficient: Single branch
function toSigned256(value: bigint): bigint {
const MAX_INT256 = 1n << 255n;
if (value >= MAX_INT256) {
return value - (1n << 256n); // Subtract modulus
}
return value;
}
// Equivalent but more complex:
function toSigned256Alt(value: bigint): bigint {
if (value & (1n << 255n)) { // Check sign bit
return -(((~value) & ((1n << 256n) - 1n)) + 1n); // Two's complement
}
return value;
}
Comparison Patterns
// Check if negative (most common pattern)
assembly {
let isNeg := slt(value, 0) // 3 gas
}
// Equivalent but more expensive:
assembly {
let signBit := shr(255, value) // 3 gas
let isNeg := eq(signBit, 1) // 3 gas (total: 6 gas)
}
Benchmarks
SLT performance matches other comparison operations:
Execution time (relative):
- SLT: 1.05x (slightly slower due to sign conversion)
- LT/GT/EQ: 1.0x
- SGT: 1.05x
- ISZERO: 0.95x
Gas efficiency:
- 3 gas per signed comparison
- ~333,333 comparisons per million gas
- Sign conversion adds negligible overhead
References
- SGT - Signed greater than
- LT - Unsigned less than
- GT - Unsigned greater than
- SDIV - Signed division
- SMOD - Signed modulo