Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x11 |
| Mnemonic | GT |
| Gas | 3 |
| Stack Input | a, b |
| Stack Output | a > b (1 if true, 0 if false) |
| Behavior | Unsigned 256-bit greater-than comparison. Both operands are treated as unsigned integers. Returns 1 or 0. |
Threat Surface
GT is the unsigned greater-than counterpart to LT. Mathematically, GT(a, b) == LT(b, a), and they share the same fundamental threat class. GT appears frequently in:
- Threshold enforcement:
require(amount > 0),require(collateralRatio > MIN_RATIO) - Time-lock release conditions:
require(block.timestamp > unlockTime) - Withdrawal conditions:
require(balance > withdrawalAmount) >=compilation: Solidity’sa >= boften compiles toISZERO(LT(a, b)), buta > bcompiles directly toGT(a, b)
The same two fundamental risks from LT apply: unsigned-only semantics (misapplied to signed data) and comparison on overflow-corrupted values. GT has an additional nuance: it’s the opcode most commonly used in time-based access control (block.timestamp > deadline), making it a primary target for timestamp manipulation attacks.
Smart Contract Threats
T1: Unsigned Comparison on Signed Values (Critical)
Identical to LT’s T1 but with inverted consequences. When GT is used on values that should be signed:
GT(int256(-1), int256(100))evaluates asGT(2^256 - 1, 100)= 1 (true)- The contract believes -1 is greater than 100
This is dangerous in:
- PnL calculations: A negative profit appears as an enormous positive value, triggering profit distribution instead of loss accounting.
- Voting weight comparisons: Negative penalties (slashing) appear as massive positive weights.
- Assembly/Yul code:
if gt(signedVar, threshold)uses unsigned GT whensgtwas needed.
T2: Off-By-One in Greater-Than Checks (High)
The difference between > and >= in threshold checks determines whether the exact threshold value is permitted:
// Allows withdrawal only when balance strictly exceeds amount
require(balance > amount); // GT: fails when balance == amount
// Allows withdrawal when balance equals or exceeds amount
require(balance >= amount); // ISZERO(LT(balance, amount)): passes when balance == amountIn time-lock contracts, require(block.timestamp > unlockTime) prevents withdrawal at exactly unlockTime, while require(block.timestamp >= unlockTime) allows it. For long vesting schedules, this one-second difference is negligible, but for Dutch auctions and flash-loan-sensitive operations where transactions compete for exact block timestamps, the off-by-one can be exploited.
T3: Time-Lock Bypass via Overflow (High)
GT is the standard opcode for time-lock release: require(block.timestamp > unlockTime). If unlockTime is computed with unchecked arithmetic (pre-0.8.0 or in unchecked blocks) and overflows to a small value, the GT check passes immediately:
// Pre-0.8.0
function extendLock(uint256 additionalSeconds) external {
unlockTime[msg.sender] += additionalSeconds; // Overflow wraps to small value
}
// GT(block.timestamp, wrappedSmallValue) = true -- funds unlocked immediatelyT4: Zero-Amount Bypass (Medium)
Many contracts use require(amount > 0) (compiled to GT(amount, 0)) to reject zero-value operations. But if the amount is computed from arithmetic that overflows or truncates to zero, this check alone is insufficient:
function swap(uint256 amountIn) external {
uint256 amountOut = computeOutput(amountIn); // Could return 0 due to rounding
require(amountOut > 0); // GT(amountOut, 0) -- passes only if non-zero
// But what if amountOut rounds to 1 wei? Technically non-zero but economically worthless
}Attackers exploit rounding to get amountOut = 1 wei that passes the GT check but drains precision value from the pool over many transactions.
T5: Block Timestamp Manipulation (Medium)
EVM block timestamps are set by validators/proposers with some tolerance (typically ±15 seconds). GT-based time checks are vulnerable to minor manipulation:
- A validator can set
block.timestampslightly in the future to satisfyGT(block.timestamp, deadline)one block early - For MEV-sensitive operations (auctions, liquidations), this gives the proposer an unfair advantage
Protocol-Level Threats
P1: No DoS Vector (Low)
GT costs a fixed 3 gas with no dynamic component. Purely stack-based.
P2: Consensus Safety (Low)
Unsigned greater-than comparison is trivially deterministic. All EVM implementations agree. No known consensus divergence.
P3: No State Impact (None)
GT modifies only the stack.
P4: Compilation Pattern Variance (Low)
Solidity compiles >= as ISZERO(LT(a, b)) rather than GT(a, b) | EQ(a, b). Understanding this is relevant for bytecode auditors who need to distinguish between > and >= at the opcode level. The pattern GT(a, b) always means strict greater-than.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
GT(0, 0) | Returns 0 (false) | Correct: 0 is not greater than 0 |
GT(1, 0) | Returns 1 (true) | Base case; common amount > 0 check |
GT(0, MAX_UINT256) | Returns 0 (false) | Correct for unsigned |
GT(MAX_UINT256, 0) | Returns 1 (true) | If value represents -1 (signed), comparison is inverted |
GT(2^255, 2^255 - 1) | Returns 1 (true) | For signed: -2^255 > 2^255 - 1 would be false; unsigned says true |
GT(a, a) | Returns 0 (false) | Strict greater-than; equal values return false |
GT(block.timestamp, smallOverflowedValue) | Returns 1 (true) | Time-lock bypass: overflowed unlock time is in the past |
GT(amount, 0) where amount was rounded down | Returns 0 or 1 | 1 wei passes the check but may be economically meaningless |
Real-World Exploits
Exploit 1: Time-Lock Overflow Bypass (OWASP Canonical Pattern)
Root cause: Integer overflow in lock time extension causes unlockTime to wrap to a small value, bypassing the GT-based time check.
Details: The OWASP Smart Contract Top 10 documents the “TimeWrapVault” pattern as a canonical exploit. A contract allows users to extend their lock period by adding seconds. In pre-0.8.0 Solidity, if a user passes type(uint256).max - currentUnlockTime + 1 as the additional seconds, the addition overflows and wraps unlockTime to 0 (or a small value). The subsequent require(block.timestamp > unlockTime) check (GT) passes immediately, allowing premature fund withdrawal.
GT’s role: GT is the security gate for time-based access control. It functions correctly on its inputs, but when the unlock time has been corrupted by overflow, the gate opens when it shouldn’t.
References:
- OWASP Smart Contract Top 10: Integer Overflow and Underflow
- Smart Contract Hacking: Arithmetic Overflow Attacks
Exploit 2: ResupplyFi — Exchange Rate Collapse Bypasses Solvency Check (June 2025)
Root cause: Exchange rate manipulation collapsed vault share price to near-zero, causing solvency ratio calculations to produce 0, which bypassed GT-based health checks.
Details: ResupplyFi’s lending protocol used an ERC4626 vault whose exchange rate could be manipulated through a first-depositor attack variant. The attacker inflated the vault’s totalAssets relative to totalSupply, collapsing the share price. When the protocol calculated the loan-to-value (LTV) ratio, the result was 0 due to integer truncation. The solvency check require(ltv > MIN_LTV) (GT-based) produced inconsistent results — positions appeared healthy with LTV of 0 when they were actually insolvent.
GT’s role: The GT-based solvency check was the protocol’s defense against undercollateralized borrowing. When the exchange rate was manipulated to produce truncated-to-zero LTV values, the comparison became meaningless, and $9.56M was drained.
References:
Exploit 3: Off-By-One in Liquidation Logic (Generic Pattern)
Root cause: Using > instead of >= in liquidation threshold comparison, preventing liquidation of positions at exactly the threshold.
Details: Multiple lending protocols have experienced issues where positions at exactly the minimum collateralization ratio cannot be liquidated due to strict greater-than checks. A position with exactly 150% collateral ratio passes require(ratio > 150) as false, making it unliquidatable. If the collateral value drops from 151% to 150% in one price update, there’s a window where the position is undercollateralized by the protocol’s intent but immune to liquidation by the code’s comparison logic.
GT’s role: The strict > (GT) excludes the boundary value. Using >= (ISZERO(LT(...))) would include it. This is the most common off-by-one pattern in DeFi protocols and has appeared in audit findings for numerous protocols.
Attack Scenarios
Scenario A: Signed/Unsigned Confusion in Funding Rate
contract VulnerablePerpetual {
function applyFundingRate(int256 rate, uint256 position) external {
uint256 payment;
assembly {
// BUG: gt() is unsigned. If rate is negative (e.g., int256(-50)),
// it looks like a huge positive number, always "greater than" 0
if gt(rate, 0) {
// Intended: positive rate means longs pay shorts
// Actual: negative rate also enters this branch!
payment := mul(position, rate) // Enormous payment computed
}
}
_transferFunding(msg.sender, payment);
}
}Scenario B: Time-Lock Bypass via Overflow
// Solidity < 0.8.0
contract VulnerableTimelock {
mapping(address => uint256) public unlockTime;
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
unlockTime[msg.sender] = block.timestamp + 1 weeks;
}
function extendLock(uint256 extraSeconds) external {
unlockTime[msg.sender] += extraSeconds; // Overflow wraps to small value
}
function withdraw() external {
require(block.timestamp > unlockTime[msg.sender]); // GT passes if overflowed
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}Scenario C: Rounding Bypass of Zero-Amount Check
contract VulnerableAMM {
function swap(uint256 amountIn) external {
// amountOut = (amountIn * reserveOut) / (reserveIn + amountIn)
// For tiny amountIn relative to reserveIn, this rounds to 0
uint256 amountOut = (amountIn * reserveOut) / (reserveIn + amountIn);
require(amountOut > 0); // GT: passes if amountOut == 1 wei
// Attacker swaps repeatedly with minimum input to extract 1 wei each time
// Each swap also shifts the reserves, amplifying rounding in the attacker's favor
_executeSwap(msg.sender, amountIn, amountOut);
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Signed/unsigned confusion | Use SGT for signed comparisons in assembly | sgt(a, b) not gt(a, b) for int256 values |
| T2: Off-by-one | Explicitly document whether boundary is inclusive or exclusive | Test at boundary, boundary-1, boundary+1; use >= when inclusive |
| T3: Time-lock overflow | Use checked arithmetic for all time calculations | Solidity >= 0.8.0 default; cap maximum extension periods |
| T4: Zero-amount bypass | Enforce minimum economic thresholds, not just non-zero | require(amountOut >= MIN_OUTPUT) instead of > 0 |
| T5: Timestamp manipulation | Design for ±15 second tolerance in time-sensitive operations | Use block.number for relative timing; avoid sub-minute precision |
Compiler/EIP-Based Protections
- Solidity 0.8.0+: Automatic overflow checks prevent time-lock and arithmetic bypass. The compiler uses GT/LT correctly based on Solidity types (uint vs int).
- Solidity type system:
int256 > int256compiles to SGT automatically. GT is only emitted for unsigned comparisons in high-level Solidity. - Slither detectors:
timestampdetector flags block.timestamp usage in comparisons.incorrect-equalityflags strict equality where comparison operators should be used.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Medium | Assembly-level DeFi bugs |
| T2 | Smart Contract | High | High | Liquidation off-by-one in multiple protocols |
| T3 | Smart Contract | High | High (pre-0.8) / Low (post-0.8) | OWASP TimeWrapVault pattern |
| T4 | Smart Contract | Medium | Medium | Rounding exploitation in AMMs |
| T5 | Smart Contract | Medium | Medium | MEV-sensitive operations |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| LT (0x10) | Unsigned less-than; LT(a, b) == GT(b, a). Mirror of GT with identical threat class |
| SGT (0x13) | Signed greater-than; must be used instead of GT when comparing int256 values |
| SLT (0x12) | Signed less-than; signed counterpart to LT |
| EQ (0x14) | Equality; GT and EQ combined express >= |
| ISZERO (0x15) | Used to negate GT: a <= b compiles to ISZERO(GT(a, b)) |
| TIMESTAMP (0x42) | Block timestamp; GT is used in time-lock release patterns against TIMESTAMP |
| ADD (0x01) | Overflow in ADD can corrupt values compared by GT |