Opcode Summary

PropertyValue
Opcode0x13
MnemonicSGT
Gas3
Stack Inputa, b
Stack Outputa > b (1 if true, 0 if false), signed comparison
BehaviorSigned 256-bit greater-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

SGT is the signed greater-than counterpart to SLT. Mathematically, SGT(a, b) == SLT(b, a). It shares the same fundamental threat class as SLT — the signed/unsigned interpretation boundary at 2^255 — but SGT appears in distinct usage patterns:

  • Positive-value validation: require(amount > 0) for int256 values uses SGT
  • Profit/PnL thresholds: if (pnl > minProfit) where PnL can be negative
  • Signed overflow detection: Checking whether a result is “too positive” (crossed the 2^255 - 1 boundary)
  • Rate comparisons: Interest rates, funding rates, and price deltas that can be positive or negative

SGT’s primary additional risk beyond SLT is its role in positive-value gates — the require(x > 0) pattern for signed values. If SGT is accidentally used on unsigned values, values >= 2^255 appear negative and fail the > 0 check, creating a denial-of-service for large unsigned values. Conversely, if GT (unsigned) is used where SGT was intended, negative values appear as enormous positive numbers and pass upper-bound checks they should fail.


Smart Contract Threats

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

When SGT is applied to unsigned values, any value >= 2^255 is interpreted as negative by the two’s complement convention:

  • SGT(2^255, 0) returns 0 (false): SGT says -2^255 is NOT greater than 0
  • GT(2^255, 0) returns 1 (true): GT correctly says 2^255 > 0

If a contract uses SGT for a positive-value check on what’s actually a uint256:

// BUG: sgt treats largeUint as negative
if iszero(sgt(amount, 0)) { revert(0, 0) }  // "require(amount > 0)" but signed
// amount = 2^255 → sgt(-2^255, 0) = 0 → iszero(0) = 1 → reverts!
// Legitimate large deposit rejected

T2: GT on Signed Values — Negative Values Appear Huge (Critical)

The inverse confusion: using unsigned GT where SGT was needed causes negative values to appear as enormous positive numbers:

  • GT(int256(-1), int256(1000)) evaluates as GT(2^256 - 1, 1000) = 1 (true)
  • The contract believes -1 is greater than 1000

This is exploitable in:

  • Reward calculations: A negative penalty (slashing) appears as an enormous positive reward
  • Voting power: Negative votes appear as massive positive voting weight
  • Position sizing: A negative PnL appears as enormous profit, triggering incorrect payouts

T3: Two’s Complement Boundary — Sign Flip Without Exception (High)

Identical to SLT’s T2: arithmetic that crosses the 2^255 boundary flips the sign without any EVM exception. After such overflow, SGT-based comparisons produce inverted results:

  • int256(2^255 - 1) + int256(1) = 2^255 unsigned = -2^255 signed
  • Before addition: SGT(value, 0) = 1 (true, value is positive)
  • After addition: SGT(value, 0) = 0 (false, value “became” negative)

A sum of positive numbers that overflows becomes negative, and SGT correctly reports it as not-greater-than-zero. But the contract’s invariant (that a sum of positive inputs should be positive) is broken.

T4: Signed Range Validation Gaps (High)

Validating that a signed value falls within a range [lower, upper] requires two checks:

require(int256(value) > lower && int256(value) < upper);
// Compiles to: SGT(value, lower) && SLT(value, upper)

If either check uses the wrong opcode (GT instead of SGT, or LT instead of SLT), the range check has a gap for values in the “negative as unsigned” region. For example, if the lower bound check uses unsigned GT:

// BUG: gt (unsigned) for lower bound, slt (signed) for upper bound
let valid := and(gt(value, lower), slt(value, upper))
// For value = -50 (0xFF...CE), lower = -100 (0xFF...9C):
// gt(0xFF...CE, 0xFF...9C) = 1 (correct by accident for these specific values)
// But gt(0xFF...CE, 0) = 1 (unsigned says -50 > 0, which is WRONG for signed)

T5: Inconsistent Ordering with GT (Medium)

SGT and GT define different total orderings on the 256-bit value space. Any algorithm that mixes signed and unsigned greater-than comparisons (e.g., a sorting algorithm that uses SGT in one comparison and GT in another) produces an inconsistent ordering that can corrupt data structures, break binary search, or cause infinite loops.


Protocol-Level Threats

P1: No DoS Vector (Low)

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

P2: Consensus Safety (Low)

Signed greater-than comparison via two’s complement is deterministic and well-specified. All EVM implementations agree.

P3: No State Impact (None)

SGT modifies only the stack.

P4: Signed Comparison Overhead (Low)

Signed range validation requires two SLT/SGT checks plus an AND, while unsigned range validation requires only one LT/GT check (since unsigned values can’t go below 0). This makes signed bounds checking slightly more gas-expensive and more error-prone, especially in assembly.


Edge Cases

Edge CaseBehaviorSecurity Implication
SGT(0, 0)Returns 0 (false)Correct: 0 is not greater than 0
SGT(0, -1)Returns 1 (true)Correct: 0 > -1 in signed
SGT(-1, 0)Returns 0 (false)Correct: -1 < 0. Note: GT(0xFF...FF, 0) returns 1 (opposite!)
SGT(2^255 - 1, -2^255)Returns 1 (true)Max > Min: correct
SGT(-2^255, 2^255 - 1)Returns 0 (false)Min < Max: correct
SGT(1, -1)Returns 1 (true)Correct
SGT(-1, -2)Returns 1 (true)-1 > -2: correct
SGT(2^255, 0)Returns 0 (false)2^255 unsigned = -2^255 signed: SGT says -2^255 is NOT > 0
SGT(int256.max, int256.max)Returns 0 (false)Equal: not greater-than

Real-World Exploits

Exploit 1: Signed Overflow in DeFi Profit Calculations (Audit Finding Pattern)

Root cause: Summation of positive profit values overflows int256.max, flipping the sign to negative, causing SGT-based profit checks to fail.

Details: A recurring pattern in DeFi protocol audits: contracts track cumulative profit as int256 (to allow for negative periods). When cumulative profit approaches 2^255 - 1 and a new positive profit is added, the result overflows to a negative value. The subsequent check require(totalProfit > 0) (SGT) fails, triggering emergency shutdowns, pausing withdrawals, or reverting legitimate operations. While 2^255 is astronomically large for most practical purposes, it becomes reachable when:

  • Values are denominated in wei (10^18 precision)
  • Accumulated over long periods
  • Using smaller signed types (int128, int96) for gas-packed storage

SGT’s role: SGT correctly reports that the overflowed value is negative. The bug is that the arithmetic overflowed, but SGT is the mechanism through which the overflow manifests as a functional failure.


Exploit 2: Unsigned/Signed Mismatch in Permit Deadline Validation

Root cause: A deadline stored as uint256 is compared with a signed comparison opcode in assembly, causing deadlines with high bit set to appear as past timestamps.

Details: Some optimized ERC-20 permit() implementations use inline assembly for gas efficiency. If the deadline comparison accidentally uses sgt instead of gt:

// Checking: block.timestamp <= deadline (i.e., deadline >= block.timestamp)
// Correct: iszero(lt(deadline, timestamp))
// Buggy: iszero(slt(deadline, timestamp))

For deadlines set to type(uint256).max (common practice for “no expiry”), sgt(timestamp, 0xFFF...FFF) treats the deadline as -1 (signed), which is less than any positive timestamp. The permit would be rejected as expired even though it’s meant to never expire.

SGT’s role: The signed comparison inverts the meaning of type(uint256).max from “maximum value” (never expires) to “-1” (always expired), creating a denial-of-service on permits with max deadline.


Exploit 3: Compiler-Level Signed Overflow — int256.min Negation

Root cause: Attempting to negate int256.min (-2^255) overflows because 2^255 exceeds int256.max (2^255 - 1). The result remains -2^255.

Details: Solidity >= 0.8.0 reverts on int256 x = -type(int256).min; but pre-0.8.0 and unchecked blocks silently produce -2^255 again. If a contract uses SGT to check whether the negation is positive:

function absoluteValue(int256 x) internal pure returns (uint256) {
    if (x < 0) {
        unchecked {
            x = -x;  // If x == int256.min, this overflows back to int256.min
        }
    }
    require(x > 0, "must be positive");  // SGT: fails because x is still negative!
    return uint256(x);
}

The function is supposed to return the absolute value but reverts (or returns a negative value cast to uint256) for int256.min. This edge case has been found in multiple DeFi protocols that handle signed values.

References:


Attack Scenarios

Scenario A: Negative Penalty Appears as Reward

contract VulnerableStaking {
    function claimReward(int256 rewardDelta) external {
        // Reward delta can be negative (slashing) or positive (reward)
        uint256 payout;
        assembly {
            // BUG: gt() is unsigned. A negative delta (e.g., int256(-100))
            // looks like MAX_UINT256 - 99, which is "greater than" 0
            if gt(rewardDelta, 0) {
                payout := rewardDelta  // Pays out ~2^256 tokens!
            }
            // Should use: if sgt(rewardDelta, 0) {
        }
        token.transfer(msg.sender, payout);
    }
}

Scenario B: Range Validation with Mixed Opcodes

contract VulnerableOracle {
    int256 constant MIN_PRICE = -1000e18;
    int256 constant MAX_PRICE = 1000000e18;
    
    function validatePrice(int256 price) internal pure {
        assembly {
            // BUG: uses gt (unsigned) for lower bound check
            // For price = int256(-500e18):
            //   gt(0xFF..., 0xFF...) -- depends on specific values, may pass or fail
            //   unpredictably compared to sgt behavior
            let aboveMin := gt(price, MIN_PRICE)   // Should be sgt
            let belowMax := slt(price, MAX_PRICE)   // Correct: signed
            if iszero(and(aboveMin, belowMax)) { revert(0, 0) }
        }
    }
}

Scenario C: Signed Overflow Breaks Invariant

contract VulnerableFund {
    int256 public totalPnL;
    
    function recordTrade(int256 profit) external {
        require(profit > 0);  // SGT: only record positive profits
        
        unchecked {
            totalPnL += profit;  // Overflows from int256.max to int256.min
        }
        
        // After overflow: totalPnL is now negative despite only adding positive values
        // Any downstream check using SGT(totalPnL, 0) fails
        // Fund appears to have negative total PnL, triggering liquidation
    }
}

Mitigations

ThreatMitigationImplementation
T1: SGT on unsigned valuesNever use SGT for uint256 comparisonsIn Yul: gt(a, b) for unsigned, sgt(a, b) for signed
T2: GT on signed valuesNever use GT for int256 comparisonsLet Solidity compiler handle opcode selection; audit assembly
T3: Sign flip at boundaryUse checked arithmetic for signed operationsSolidity 0.8.0+ default; explicit SafeCast for type conversions
T3: int256.min negationSpecial-case the minimum valuerequire(x != type(int256).min) before negation
T4: Range validation gapsUse consistent opcode family for both boundsBoth bounds must use slt/sgt or both use lt/gt
T5: Ordering inconsistencyUse a single comparison family throughout data structuresDocument and enforce signed vs unsigned choice per data type

Compiler/EIP-Based Protections

  • Solidity type system: Automatically selects SGT for int256 > int256 and GT for uint256 > uint256. Risk is confined to assembly blocks.
  • Solidity 0.8.0+: Reverts on signed overflow, preventing the most dangerous sign-flip scenarios.
  • SafeCast (OpenZeppelin): toUint256(int256) and toInt256(uint256) revert on invalid conversions, preventing cross-type confusion.
  • Formal verification: Tools like Certora and Halmos can verify that signed comparisons are used consistently and that signed overflow cannot occur for bounded inputs.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalMediumAssembly bugs in gas-optimized contracts
T2Smart ContractCriticalMediumReward/penalty sign confusion
T3Smart ContractHighMedium (pre-0.8) / Low (post-0.8)Profit accumulator overflow
T4Smart ContractHighLowMixed opcode range validation in assembly
T5Smart ContractMediumLowSorting/ordering bugs
P1ProtocolLowN/A
P2ProtocolLowN/A

OpcodeRelationship
SLT (0x12)Signed less-than; SLT(a, b) == SGT(b, a). Same signed threat class, mirrored operands
GT (0x11)Unsigned greater-than; SGT and GT produce different results when either operand has bit 255 set
LT (0x10)Unsigned less-than; the unsigned counterpart to SLT
SIGNEXTEND (0x0B)Extends smaller signed types to 256 bits; must be applied before SGT for sub-256-bit signed values
SDIV (0x05)Signed division; the int256.min / -1 edge case affects SGT-based positive-value checks
SMOD (0x07)Signed modulus; shares the two’s complement boundary concerns with SGT
ISZERO (0x15)Negates SGT results: a <= b (signed) compiles to ISZERO(SGT(a, b))