Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x13 |
| Mnemonic | SGT |
| Gas | 3 |
| Stack Input | a, b |
| Stack Output | a > b (1 if true, 0 if false), signed comparison |
| Behavior | Signed 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)returns0(false): SGT says -2^255 is NOT greater than 0GT(2^255, 0)returns1(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 rejectedT2: 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 asGT(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^255unsigned =-2^255signed- 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 Case | Behavior | Security 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
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: SGT on unsigned values | Never use SGT for uint256 comparisons | In Yul: gt(a, b) for unsigned, sgt(a, b) for signed |
| T2: GT on signed values | Never use GT for int256 comparisons | Let Solidity compiler handle opcode selection; audit assembly |
| T3: Sign flip at boundary | Use checked arithmetic for signed operations | Solidity 0.8.0+ default; explicit SafeCast for type conversions |
| T3: int256.min negation | Special-case the minimum value | require(x != type(int256).min) before negation |
| T4: Range validation gaps | Use consistent opcode family for both bounds | Both bounds must use slt/sgt or both use lt/gt |
| T5: Ordering inconsistency | Use a single comparison family throughout data structures | Document and enforce signed vs unsigned choice per data type |
Compiler/EIP-Based Protections
- Solidity type system: Automatically selects SGT for
int256 > int256and GT foruint256 > 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)andtoInt256(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 ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Medium | Assembly bugs in gas-optimized contracts |
| T2 | Smart Contract | Critical | Medium | Reward/penalty sign confusion |
| T3 | Smart Contract | High | Medium (pre-0.8) / Low (post-0.8) | Profit accumulator overflow |
| T4 | Smart Contract | High | Low | Mixed opcode range validation in assembly |
| T5 | Smart Contract | Medium | Low | Sorting/ordering bugs |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| 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)) |