Opcode Summary

PropertyValue
Opcode0x12
MnemonicSLT
Gas3
Stack Inputa, b
Stack Outputa < b (1 if true, 0 if false), signed comparison
BehaviorSigned 256-bit less-than comparison using two’s complement. Interprets both operands as signed integers in the range [-2^255, 2^255 - 1]. Returns 1 or 0.

Threat Surface

SLT is the signed counterpart to LT. While LT treats all 256-bit values as unsigned integers in [0, 2^256 - 1], SLT interprets them as two’s complement signed integers in [-2^255, 2^255 - 1]. The boundary between “positive” and “negative” falls at 2^255:

  • Values 0x00...00 to 0x7FFF...FFFF (0 to 2^255 - 1) are positive
  • Values 0x8000...0000 to 0xFFFF...FFFF (2^255 to 2^256 - 1 unsigned) are negative (-2^255 to -1)

The threat surface centers on the signed/unsigned interpretation boundary:

  1. Using SLT on unsigned values: If a value is logically unsigned (e.g., a token balance, array index, or gas amount) but compared with SLT, any value >= 2^255 is treated as negative. A balance of 2^255 (a legitimate large unsigned value) is interpreted as the most negative possible number (-2^255), completely inverting the comparison.

  2. Two’s complement boundary exploitation: The transition from 0x7FFF...FFFF (max positive: 2^255 - 1) to 0x8000...0000 (min negative: -2^255) is a single increment. Arithmetic that pushes a signed value across this boundary causes a sign flip without any overflow exception, potentially inverting all downstream comparison logic.

  3. Cross-domain confusion: When signed values from one context (e.g., a price delta from an oracle) interact with unsigned values from another context (e.g., a token amount), using the wrong comparison opcode produces silently incorrect results.


Smart Contract Threats

T1: SLT on Unsigned Values — Large Balances Appear Negative (Critical)

When SLT is used to compare values that are logically unsigned, any value with the high bit set (>= 2^255) is treated as negative. This effectively halves the usable unsigned range and inverts comparisons for large values:

  • A token with total supply > 2^255 (possible with low-decimal tokens or rebasing tokens) would have balances that SLT considers negative
  • SLT(2^255, 100) returns 1 (true): SLT says “negative number < 100”
  • LT(2^255, 100) returns 0 (false): LT correctly says “large number >= 100”

In assembly or Yul, accidentally using slt instead of lt for unsigned comparisons is a critical bug:

// BUG: slt treats largeBalance as negative
if slt(balance, minRequired) { revert(0, 0) }
// balance = 2^255 (valid uint256) is treated as -2^255, "less than" minRequired
// Transaction reverts despite having more than enough balance

T2: Two’s Complement Sign Boundary Overflow (Critical)

Signed integer overflow in two’s complement occurs when arithmetic crosses the boundary between 2^255 - 1 (max positive) and -2^255 (min negative). The EVM provides no signed overflow detection — ADD, SUB, and MUL all operate on the raw 256-bit representation regardless of signed interpretation:

  • (2^255 - 1) + 1 = 2^255 in unsigned math, which is -2^255 in signed
  • A positive value plus a positive value produces a negative result
  • SLT-based checks after such overflow produce inverted results

This matters in:

  • Accumulation of signed values: Summing positive deltas that push past 2^255 - 1
  • Negation overflow: int256(-2^255) cannot be negated because 2^255 doesn’t fit in int256. The expression 0 - (-2^255) overflows back to -2^255
  • SDIV edge case: SDIV(-2^255, -1) overflows because 2^255 exceeds int256.max

T3: Signed/Unsigned Mismatch in Cross-Contract Calls (High)

When Contract A encodes a value as int256 and Contract B decodes it as uint256 (or vice versa), subsequent comparisons using SLT or LT produce contradictory results:

  • Contract A sends int256(-50) (encoded as 0xFFFF...FFCE)
  • Contract B decodes it as uint256(0xFFFF...FFCE) = a very large positive number
  • Contract B uses LT(value, threshold) — the “negative” value appears enormous and passes upper-bound checks
  • Or: Contract A sends uint256(2^255 + 100) which Contract B decodes as int256(-2^255 + 100) = a very negative number

T4: Ordering Inconsistency Between Signed and Unsigned (High)

The total ordering defined by SLT differs from LT for any pair where one or both values have the high bit set:

ValuesLT ResultSLT ResultComment
(0, 1)1 (true)1 (true)Agree for small positives
(MAX_UINT256, 0)0 (false)1 (true)LT: huge > 0. SLT: -1 < 0
(2^255, 2^255 - 1)0 (false)1 (true)LT: bigger. SLT: negative < positive

Using SLT for sorting, indexing, or binary search on unsigned values produces a corrupted ordering where values above 2^255 are sorted before 0.

T5: Conditional Logic Inversion in Assembly (Medium)

Assembly-heavy contracts (optimized DEXs, precompile wrappers, custom data structures) frequently use comparison opcodes directly. Using slt where lt was intended (or vice versa) in conditional jumps silently inverts the logic for a subset of inputs — specifically, any input with the high bit set. This is particularly insidious because it may pass all tests with “normal” small values and only fail on edge-case large values.


Protocol-Level Threats

P1: No DoS Vector (Low)

SLT costs a fixed 3 gas with no dynamic component. Purely stack-based.

P2: Consensus Safety (Low)

Signed comparison on two’s complement 256-bit integers is well-defined and deterministic. All EVM implementations agree on SLT’s behavior. The two’s complement interpretation is specified in the Yellow Paper.

P3: No State Impact (None)

SLT modifies only the stack.

P4: Signed Overflow Detection Gap (Medium)

The EVM provides no native signed overflow detection opcode. Detecting signed overflow after ADD requires checking whether the sign of the result is inconsistent with the signs of the operands:

// Signed overflow: both positive but result negative, or both negative but result positive
// (a > 0 && b > 0 && result < 0) || (a < 0 && b < 0 && result > 0)

This check requires multiple SLT/SGT operations, making it more expensive and error-prone than unsigned overflow detection (a single LT). Protocol-level, this means signed arithmetic is inherently harder to secure.


Edge Cases

Edge CaseBehaviorSecurity Implication
SLT(0, 0)Returns 0 (false)Correct: 0 is not less than 0
SLT(-1, 0)Returns 1 (true)Correct: -1 < 0 in signed
SLT(0, -1)Returns 0 (false)Correct: 0 > -1. Note: LT(0, 0xFF...FF) returns 1 (opposite!)
SLT(-2^255, 2^255 - 1)Returns 1 (true)Min < Max: correct
SLT(2^255 - 1, -2^255)Returns 0 (false)Max > Min: correct
SLT(-2^255, -2^255)Returns 0 (false)Equal values: not less-than
SLT(-1, -2)Returns 0 (false)-1 > -2: correct
SLT(2^255, 0)Returns 1 (true)2^255 unsigned = -2^255 signed: SLT says negative < 0
SLT(int256.max + 1, 0)Returns 1 (true)Signed overflow: 2^255 - 1 + 1 = -2^255 in signed

Real-World Exploits

Exploit 1: Solidity SDIV Signed Overflow (Known Edge Case, Multiple Audits)

Root cause: The expression int256(-2^255) / int256(-1) should produce 2^255, but 2^255 exceeds int256.max (2^255 - 1), causing silent overflow.

Details: Two’s complement has an asymmetric range: [-2^255, 2^255 - 1]. The minimum value -2^255 has no positive counterpart that fits in int256. Dividing it by -1 (which should negate it) overflows. The EVM’s SDIV returns -2^255 unchanged in this case. If a contract uses SLT to check whether a division result is positive (SLT(0, result)) after this operation, it gets false (the result is negative), even though mathematically the quotient should be positive. This has been flagged in multiple security audits of DeFi protocols that handle signed arithmetic.

SLT’s role: SLT correctly reports that the overflowed value is negative, but the developer expected a positive result. The bug is in the arithmetic, not the comparison, but SLT propagates the incorrect sign into downstream logic.

References:


Exploit 2: Signed/Unsigned Confusion in Price Feed Processing

Root cause: Price delta from an oracle (signed, can be negative) compared with an unsigned threshold using the wrong comparison opcode.

Details: A pattern found in multiple audit reports: DeFi protocols receive signed price deltas from Chainlink or similar oracles. When processing these in Yul/assembly for gas efficiency, developers sometimes use lt (unsigned) instead of slt (signed) to check whether a price change is below a maximum allowed deviation. A negative price change (e.g., -5%) is represented as a large unsigned number, which passes an unsigned upper-bound check that should have rejected it as out-of-range.

// Intended: reject if absolute price change > 10%
// BUG: lt is unsigned; negative delta (0xFF...F6 = -10 in signed) > maxDelta unsigned
let withinBounds := lt(priceDelta, maxDelta)  // Always false for negative deltas!
// Fix: need separate sign check with slt, then compare magnitude

SLT’s role: The correct implementation requires SLT to first determine the sign of the delta, then compare the magnitude. Using unsigned LT for what is fundamentally a signed comparison is the core vulnerability.


Exploit 3: BEC/SMT Overflow — Signed Interpretation of Overflow Results

Root cause: While BEC was fundamentally an unsigned overflow, the overflowed values cross the 2^255 boundary, meaning any subsequent signed comparison would produce inverted results.

Details: In the BEC batchOverflow exploit, the attacker passed _value = 2^255. After the overflow in cnt * _value, any contract that subsequently checked the inflated balances using signed comparison (SLT) would see 2^255 as -2^255 — the most negative possible value. This doesn’t directly affect BEC (which used unsigned comparisons), but it illustrates the danger: if a downstream contract receives tokens with balance 2^255 and uses signed arithmetic/comparisons, the balance appears as an enormous negative number, potentially breaking invariants.

References:


Attack Scenarios

Scenario A: SLT on Unsigned Token Balance

contract VulnerableVault {
    function hasMinimumDeposit(uint256 amount) internal pure returns (bool) {
        bool sufficient;
        assembly {
            // BUG: slt() interprets amount as signed
            // If amount >= 2^255 (e.g., a high-supply token), it appears negative
            sufficient := slt(999, amount)  // "Is 999 < amount?"
            // For amount = 2^255: slt(999, -2^255) = false (999 > -2^255)
            // Deposit of 2^255 tokens rejected as "insufficient"
        }
        return sufficient;
    }
}

Scenario B: Signed Overflow in Accumulator

contract VulnerableAccumulator {
    int256 public totalPnL;
    
    function recordProfit(int256 amount) external {
        require(amount > 0);  // SGT: positive profits only
        
        // BUG: no signed overflow check. If totalPnL is near int256.max,
        // adding a positive amount overflows to a negative value
        totalPnL += amount;
        
        // totalPnL was 2^255 - 10, amount is 100
        // New totalPnL = 2^255 + 90, which is -2^255 + 90 in signed
        // SLT(totalPnL, 0) now returns true -- massive profit looks like a loss
        
        if (totalPnL < 0) {
            _triggerEmergencyShutdown();  // False alarm from signed overflow
        }
    }
}

Scenario C: Cross-Contract Type Confusion

interface IOracle {
    function getPrice() external view returns (int256);  // Can be negative (funding rate)
}
 
contract VulnerableConsumer {
    function processPrice(address oracle) external {
        // Decoded as uint256 due to bug (wrong interface or raw call decoding)
        uint256 price = uint256(IOracle(oracle).getPrice());
        
        // If oracle returns int256(-50), price = 0xFF...CE (huge unsigned number)
        require(price < MAX_PRICE);  // LT: 0xFF...CE < MAX_PRICE? Probably false.
        // But require(price > MIN_PRICE) passes because 0xFF...CE > anything small
        
        // The negative price, now a huge unsigned value, is used in calculations
        _executeTrade(price);  // Trade at an astronomically inflated price
    }
}

Mitigations

ThreatMitigationImplementation
T1: SLT on unsigned valuesNever use SLT for uint256 comparisonsIn Yul: lt(a, b) for unsigned, slt(a, b) for signed. Solidity handles this automatically
T2: Signed overflow at boundaryCheck for signed overflow after arithmeticrequire(!(a > 0 && b > 0 && result < 0)) for addition
T2: Negation overflowSpecial-case int256.minrequire(a != type(int256).min) before negation
T3: Cross-contract type mismatchEnforce consistent typing at ABI boundariesUse explicit interface types; validate ranges on decoded signed values
T4: Ordering inconsistencyUse consistent comparison across a codebaseDocument whether each comparison point uses signed or unsigned semantics
T5: Assembly confusionCode review for slt/lt confusion in YulLint rules or static analysis to flag slt usage on known-unsigned variables

Compiler/EIP-Based Protections

  • Solidity type system: The compiler automatically emits SLT for int256 < int256 and LT for uint256 < uint256. Signed/unsigned confusion is primarily a risk in inline assembly.
  • Solidity 0.8.0+: Automatic revert on signed integer overflow (int256.max + 1 reverts). This prevents the most dangerous signed overflow patterns.
  • SafeCast (OpenZeppelin): Provides checked conversions between signed and unsigned types: toInt256(uint256 value) reverts if value > type(int256).max.
  • Static analysis: Slither and Mythril can detect signed/unsigned type confusion in some patterns.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalMediumAssembly bugs in gas-optimized contracts
T2Smart ContractCriticalMedium (pre-0.8) / Low (post-0.8)SDIV edge case in multiple audits
T3Smart ContractHighMediumOracle/price feed type mismatches
T4Smart ContractHighLowSorting/indexing bugs with mixed types
T5Smart ContractMediumMediumAssembly-heavy DEX code
P1ProtocolLowN/A
P2ProtocolLowN/A
P4ProtocolMediumLowSigned overflow detection complexity

OpcodeRelationship
LT (0x10)Unsigned less-than; SLT and LT produce different results for any pair where one or both values have bit 255 set
SGT (0x13)Signed greater-than; SGT(a, b) == SLT(b, a). Same signed threat class
GT (0x11)Unsigned greater-than; the unsigned counterpart to SGT
SIGNEXTEND (0x0B)Extends smaller signed types to 256 bits; incorrect sign extension before SLT comparison produces wrong results
SDIV (0x05)Signed division; shares the int256.min / -1 overflow edge case that affects SLT-based sign checks
ISZERO (0x15)Negates SLT results: a >= b (signed) compiles to ISZERO(SLT(a, b))
SUB (0x03)Subtraction can cause signed overflow that flips the sign, corrupting SLT comparisons