Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x05 |
| Mnemonic | SDIV |
| Gas | 5 |
| Stack Input | a, b |
| Stack Output | a // b (signed, truncated toward zero) |
| Behavior | Signed 256-bit integer division using two’s complement. Truncates toward zero (not floor). Division by zero returns 0. Special case: -2^255 / -1 returns -2^255 (overflow wraps). |
Threat Surface
SDIV introduces threat vectors that its unsigned counterpart DIV (0x04) does not have. The primary concern is the unique overflow edge case: type(int256).min / -1. In two’s complement, int256 ranges from -2^255 to 2^255 - 1. Dividing -2^255 by -1 should mathematically produce +2^255, but that value cannot be represented in int256 — so the EVM silently returns -2^255 (the input itself). This is the only division operation in the EVM that produces an arithmetically incorrect result, and unlike ADD/MUL overflow it happens in a single, non-obvious edge case rather than a broad range of inputs.
Beyond the overflow, SDIV’s threat surface includes:
- Silent division by zero: Like DIV, dividing by zero returns 0 with no exception.
- Signed/unsigned type confusion: The EVM stack has no type system. The same 256-bit value is interpreted completely differently by SDIV (signed) vs. DIV (unsigned). Mixing them — especially in inline assembly — produces silently wrong results.
- Truncation toward zero: SDIV truncates toward zero, not toward negative infinity. This means
-7 / 2 = -3(not-4), which differs from Python-style floor division and can surprise developers.
Smart Contract Threats
T1: The int256.min / -1 Overflow (Critical)
The most dangerous SDIV edge case: when the dividend is type(int256).min (-2^255, or 0x8000...0000) and the divisor is -1 (0xFFFF...FFFF), the EVM returns -2^255 — the dividend unchanged. The mathematically correct answer (+2^255) cannot be represented in int256, so the result wraps.
This is uniquely dangerous because:
- It is the only SDIV overflow case. Unlike ADD/MUL, which overflow across a broad input range, SDIV overflows on exactly one input pair.
- Pre-Solidity 0.8.0: No check was emitted. The operation silently produced a wrong (negative) result when a positive one was expected.
- Solidity 0.8.0+: The compiler inserts an explicit check and reverts with a panic code (0x11) for this case. However,
unchecked { }blocks and inline assembly bypass this protection entirely. - Financial impact: If a contract computes
profit / scaleFactorwhereprofitcan beint256.minandscaleFactorcan be-1, the result flips sign — a negative loss becomes a negative gain (or vice versa), corrupting accounting logic.
// Pre-0.8.0: Silently returns type(int256).min (wrong sign)
int256 result = type(int256).min / int256(-1);
// result == type(int256).min == -57896... (expected: +57896...)
// 0.8.0+: Reverts with Panic(0x11)
// But in unchecked blocks, the old behavior returns:
unchecked {
int256 result = type(int256).min / int256(-1);
// result == type(int256).min -- silently wrong
}T2: Signed/Unsigned Type Confusion (High)
The EVM stack is untyped — every value is a raw 256-bit word. SDIV interprets values as two’s complement signed integers, while DIV treats them as unsigned. Using the wrong opcode on a value produces completely different results:
| Value (hex) | DIV interpretation | SDIV interpretation |
|---|---|---|
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 2^256 - 1 (max uint) | -1 |
0x8000000000000000000000000000000000000000000000000000000000000000 | 2^255 | -2^255 (min int) |
This confusion arises in:
- Inline assembly: The developer must manually choose
divvs.sdiv. Choosing wrong produces silently incorrect results with no compiler warning. - Cross-contract interfaces: A function returning
int256interpreted asuint256(or vice versa) through ABI miscoding or low-level calls. - Yul/IR code generation: Optimizers may inadvertently substitute one for the other if type information is lost.
T3: Negative Value Handling in Financial Calculations (High)
DeFi protocols that use signed integers for PnL (profit and loss), funding rates, interest rate deltas, or price impact calculations must use SDIV correctly. Errors include:
- Sign propagation:
(-100) / 3 = -33(truncated toward zero), not-34. If a protocol expects floor division semantics (rounding toward negative infinity), the result is off by 1 for every negative division with a remainder. In a funding rate calculation applied across millions of positions, this systematic bias accumulates. - Negation before division: A common pattern is
result = -a / b. Ifa == type(int256).min, negatingaoverflows (producingtype(int256).minagain), and the subsequent division operates on the wrong value. - Asymmetric rounding:
7 / 2 = 3but-7 / 2 = -3(not-4). Protocols that rely on symmetric rounding for long/short positions may create exploitable funding imbalances.
T4: Silent Division by Zero (Medium)
SDIV returns 0 when the divisor is zero, with no exception or revert. This mirrors DIV’s behavior but is more dangerous in signed contexts where 0 is a valid and meaningful result. A contract computing priceChange / timeDelta where timeDelta unexpectedly equals zero gets 0 instead of an error — which may be silently accepted as “no price change” when the real issue is missing data.
int256 rate = totalAccrued / int256(elapsed);
// If elapsed == 0: rate = 0, silently hiding a logic error
// The contract proceeds as if the rate is zeroT5: Truncation Direction Mismatch (Medium)
SDIV truncates toward zero (rounds toward zero). This differs from:
- Python: Floor division (
//) rounds toward negative infinity - Mathematical convention: Often floor division in number theory
- Some financial standards: Rounding conventions vary
When a protocol’s specification says “divide and round down” for negative numbers, the developer may assume SDIV does this, but it actually rounds up (toward zero) for negative quotients. This can create 1-wei discrepancies that accumulate over many operations or that an attacker can exploit through repeated transactions.
Protocol-Level Threats
P1: EVM Client Implementation Divergence (Medium)
The int256.min / -1 edge case is a known source of implementation bugs in EVM clients. The correct behavior per the Yellow Paper is to return int256.min (the overflow wraps), but implementations must handle this explicitly:
- LambdaClass Ethrex (2024): FuzzingLabs discovered that the Ethrex EVM’s SDIV implementation panicked when processing certain inputs due to an overflow in its
negate()helper function. The bug caused a denial of service — the EVM crashed instead of returning a result. While the immediate trigger was0 / negative_value(negate applied to a zero quotient), the root cause was unsafe two’s complement arithmetic in the SDIV code path. - EIP-6888 (Proposed): Proposes adding explicit overflow (
sovf) flags to the EVM state, specifically for SDIV whena == INT_MIN ∧ b == -1orb == 0. This acknowledges that the current silent-overflow behavior is a consensus risk.
Any EVM client that incorrectly handles the edge case — returning a different value, reverting, or panicking — would diverge from the canonical chain, causing a consensus split.
P2: Compiler-Level Divergence for Signed Arithmetic (Low)
Different Solidity compiler versions handle signed division differently:
- Pre-0.8.0: No overflow checks. SDIV is emitted directly.
- 0.8.0+: The compiler inserts a check:
if (b == -1 && a == type(int256).min) revert Panic(0x11). This changes the observable behavior of the same Solidity source code depending on compiler version. - Signed Immutables Bug (Solidity 0.6.5–0.8.8): Signed immutable values shorter than 256 bits were not properly sign-extended when read back, causing them to be misinterpreted as positive unsigned values. While not a direct SDIV bug, it corrupted inputs to signed division operations when immutables were involved.
P3: No DoS Vector (Low)
SDIV costs a fixed 5 gas with no dynamic component. It operates purely on the stack with no memory, storage, or external access. It cannot be used for gas griefing.
Edge Cases
| Edge Case | Input | Result | Security Implication |
|---|---|---|---|
| Overflow | int256.min / -1 | int256.min (-2^255) | Unique SDIV overflow: result is negative when positive expected. Sign corruption in financial math. |
| Positive / Negative | 10 / -3 | -3 (truncated toward zero) | Truncation toward zero, not floor. Off-by-one vs. floor division. |
| Negative / Negative | -10 / -3 | 3 (truncated toward zero) | Result is positive. Correct, but verify protocol assumptions about rounding. |
| Division by zero | a / 0 | 0 | Silent failure. No revert, no exception. Can mask logic errors. |
| Negative / 1 | -5 / 1 | -5 | Identity; sign preserved correctly. |
| 0 / Negative | 0 / -7 | 0 | Correct, but EVM implementations must not apply sign to a zero quotient. (LambdaClass bug triggered here.) |
| int256.min / 1 | -2^255 / 1 | -2^255 | No overflow. Result representable. |
| int256.min / int256.min | -2^255 / -2^255 | 1 | Correct. |
| -1 / int256.min | -1 / -2^255 | 0 (truncated) | Mathematically ~0.0000…00003, truncates to 0. |
Real-World Exploits
Exploit 1: LambdaClass Ethrex EVM — SDIV Implementation Crash (2024)
Root cause: Arithmetic overflow in the negate() helper function used by the SDIV opcode handler.
Details: FuzzingLabs audited LambdaClass’s Ethrex EVM implementation and discovered that the negate() function, which computes two’s complement negation as !value + 1, lacked overflow protection. When the SDIV code path called negate(0) (because a zero quotient was incorrectly flagged as negative due to the divisor’s sign), the function computed !0 = U256::MAX, then U256::MAX + 1, triggering a panic.
The vulnerable code:
fn negate(value: U256) -> U256 {
!value + U256::one() // Panics when value == 0 (U256::MAX + 1 overflows)
}The initial patch used saturating_add, but this introduced a second bug: negate(0) now returned U256::MAX instead of 0, producing an incorrect SDIV result that deviated from the Yellow Paper specification.
Impact: Denial of service against any node running the Ethrex EVM. An attacker could craft a transaction with a specific SDIV payload (gas cost: 5 gas) to crash the node. If deployed on a network using Ethrex as a consensus client, this would cause a chain split.
SDIV’s role: The bug was directly in the SDIV opcode handler’s signed arithmetic logic. The interaction between is_negative(), negate(), and the quotient sign determination via XOR exposed both a crash and a correctness violation.
References:
Exploit 2: Solidity Signed Immutables Compiler Bug (September 2021)
CVE: N/A (compiler bug, severity rated “very low” by Solidity team)
Root cause: Signed immutable variables shorter than 256 bits were not properly sign-extended when read from bytecode, causing values to be misinterpreted.
Details: In Solidity 0.6.5 through 0.8.8, when a signed immutable variable (e.g., int8, int128) was assigned during construction, it was stored left-aligned in a 32-byte word. When read back through inline assembly, sign extension (“cleanup”) was not performed. For example, int8(-2) stored as 0xFE would be read as +254 instead of -2 when accessed via assembly.
While this is a compiler bug rather than an SDIV opcode bug, it directly corrupts inputs to SDIV operations. If a sign-unextended value is used as a dividend or divisor in inline assembly SDIV, the result is computed on the wrong numeric value.
Impact: Any contract compiled with Solidity 0.6.5–0.8.8 that reads signed immutables via inline assembly and uses them in signed arithmetic could produce incorrect results. The Solidity team rated this “very low” because standard Solidity code (without assembly) performs cleanup before operations, but auditors noted that assembly-heavy DeFi math libraries were potentially affected.
References:
Exploit 3: Notional Finance — Signed Integer Casting Vulnerability (October 2023)
Root cause: Dubious typecast from int256 to uint256 without proper bounds checking in vault share minting logic.
Details: During a Sherlock audit of Notional Finance V3, auditors identified that SingleSidedLPVaultBase._mintVaultShares() performed an unsafe cast from int256 to uint256. A negative int256 value cast to uint256 wraps to an enormous positive value due to two’s complement representation. Notional’s own SafeInt256 library included explicit checks for int256.min / -1 in its div() function, demonstrating awareness of the edge case, but the casting vulnerability bypassed these protections entirely.
Impact: Potential for minting an attacker-controlled number of vault shares by engineering a negative intermediate value that wraps to a large positive uint256.
SDIV’s role: The vulnerability chain involved signed arithmetic (using SDIV for rate calculations) producing negative intermediate values that were then unsafely cast to unsigned types. The SafeInt256 library’s explicit require(!(b == -1 && a == _INT256_MIN)) check in its division function shows this edge case was a known concern for the protocol.
References:
Attack Scenarios
Scenario A: The int256.min / -1 Overflow in an Unchecked Block
// Solidity 0.8.x with unchecked -- optimizer-friendly but dangerous
contract VulnerablePnL {
function calculateProfitShare(int256 totalPnL, int256 scaleFactor) external pure returns (int256) {
unchecked {
// Developer assumes scaleFactor is always positive
// But if scaleFactor == -1 and totalPnL == type(int256).min:
// Result is type(int256).min (negative) instead of +2^255
return totalPnL / scaleFactor;
}
}
}Attack: An attacker who controls scaleFactor (e.g., through governance manipulation or oracle poisoning) sets it to -1 while totalPnL is at type(int256).min. The result is negative instead of positive, corrupting profit distribution.
Scenario B: Signed/Unsigned Confusion in Assembly
contract TypeConfusion {
function divideValues(uint256 a, uint256 b) external pure returns (uint256) {
assembly {
// BUG: Developer used sdiv instead of div for unsigned values
// If a >= 2^255, SDIV interprets it as negative
// Example: a = 2^255 + 100, b = 2
// DIV: (2^255 + 100) / 2 = 2^254 + 50
// SDIV: (-2^255 + 100) / 2 = -2^254 + 50 (wrong for unsigned)
let result := sdiv(a, b)
mstore(0x00, result)
return(0x00, 0x20)
}
}
}Scenario C: Truncation Direction Exploit in Funding Rates
contract FundingRate {
int256 constant PRECISION = 1e18;
// Funding rate paid from longs to shorts (or vice versa)
function calculateFunding(int256 totalFunding, int256 numPositions) external pure returns (int256 perPosition) {
// SDIV truncates toward zero:
// -7 / 2 = -3 (not -4)
// +7 / 2 = +3
// Long side pays: total = numPositions * perPosition
// Longs: 2 * (-3) = -6 (should be -7)
// 1 wei of funding "disappears" each period
// Over millions of periods, this creates an exploitable imbalance
perPosition = totalFunding / numPositions;
}
}Attack: An attacker repeatedly opens and closes positions to trigger the truncation bias, slowly extracting value from the funding pool.
Scenario D: Division by Zero Masking a Logic Error
contract OracleConsumer {
function getAveragePrice(int256 priceSum, int256 count) external pure returns (int256) {
// If oracle reports 0 valid prices, count = 0
// SDIV returns 0 silently -- contract treats price as 0
// instead of reverting due to no valid data
return priceSum / count;
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: int256.min / -1 overflow | Use Solidity >= 0.8.0 (automatic panic revert) | Default behavior; the compiler inserts the check automatically |
| T1: In unchecked blocks | Add explicit check before signed division | require(!(b == -1 && a == type(int256).min), "SDIV overflow") |
| T1: In inline assembly | Manual guard before sdiv | Check both operands before executing sdiv in Yul/assembly |
| T2: Signed/unsigned confusion | Never use sdiv for unsigned values in assembly | Use div for uint256, sdiv only for int256 |
| T2: Type system bypass | Avoid raw assembly for arithmetic; let the compiler handle types | Use Solidity’s high-level int256 division instead of Yul sdiv |
| T3: Negative financial values | Use safe signed math libraries for all DeFi arithmetic | OpenZeppelin SignedMath, Notional SafeInt256, or equivalent |
| T3: Rounding direction | Choose and document rounding convention explicitly | Implement divFloor() / divCeil() wrappers for directional rounding |
| T4: Division by zero | Always validate divisor before division | require(divisor != 0, "division by zero") |
| T5: Truncation mismatch | Implement floor-division when spec requires it | a / b - (a % b != 0 && (a ^ b) < 0 ? 1 : 0) for floor semantics |
Compiler/EIP-Based Protections
- Solidity 0.8.0+ (2020): Automatic revert on signed overflow, including
int256.min / -1. Emits a Panic(0x11) error code. - SafeInt256 libraries: Pre-0.8.0 libraries (e.g., Notional’s SafeInt256, OpenZeppelin SignedSafeMath) that wrap SDIV with explicit edge-case checks.
- EIP-6888 (Proposed): Would add native overflow (
sovf) flags to the EVM state, set when SDIV encountersint256.min / -1or division by zero. This would allow contracts to branch on overflow without compiler-inserted checks. - Static analysis tools: Slither detects signed integer issues including unsafe casting and missing overflow checks. Mythril and Certora can formally verify absence of the
int256.min / -1case.
SDIV Overflow Detection at EVM Level
The standard pattern for guarding against the SDIV overflow in bytecode:
// Check: is this the int256.min / -1 case?
PUSH32 0x8000000000000000000000000000000000000000000000000000000000000000 // int256.min
DUP2 // Copy dividend
EQ // dividend == int256.min?
PUSH32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF // -1
DUP4 // Copy divisor
EQ // divisor == -1?
AND // Both conditions true?
ISZERO // Invert: 0 if overflow, 1 if safe
PUSH dst // Jump target (safe path)
JUMPI // Jump if safe
REVERT // Revert on overflow
This is the pattern Solidity 0.8.0+ emits around every int256 division.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Low (single input pair, but catastrophic if hit) | Notional SafeInt256 guard; LambdaClass EVM crash |
| T2 | Smart Contract | High | Medium (assembly-heavy DeFi code) | Solidity Signed Immutables Bug (compiler-level) |
| T3 | Smart Contract | High | Medium (any protocol using signed math for finance) | Notional Finance casting vulnerability |
| T4 | Smart Contract | Medium | Medium (same as DIV’s division-by-zero) | — |
| T5 | Smart Contract | Medium | Low (requires floor-division specification mismatch) | Funding rate rounding issues in perpetuals protocols |
| P1 | Protocol | Medium | Low (rare but catastrophic: consensus split) | LambdaClass Ethrex EVM crash |
| P2 | Protocol | Low | Low | Solidity Signed Immutables Bug |
| P3 | Protocol | Low | N/A | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| DIV (0x04) | Unsigned counterpart. Same division-by-zero behavior (returns 0), but no overflow edge case since unsigned division cannot overflow. Using DIV on signed values or SDIV on unsigned values produces wrong results. |
| SMOD (0x07) | Signed modulus, the remainder companion to SDIV. Has its own edge case: int256.min % -1 = 0 (correct, but the division that would accompany it overflows). |
| SLT (0x12) | Signed less-than comparison. Used in overflow detection patterns to check if SDIV results have unexpected signs. |
| SGT (0x13) | Signed greater-than comparison. Paired with SLT for range-checking SDIV results. |
| SIGNEXTEND (0x0B) | Sign-extends smaller signed integers to 256 bits. Critical for correct SDIV behavior when operating on values narrower than int256 (e.g., int8, int128). The Signed Immutables Bug was a failure of sign extension. |
| SAR (0x1D) | Arithmetic shift right. Preserves sign bit, unlike SHR. Sometimes used as a faster signed division by powers of 2, but with floor-division semantics (rounds toward negative infinity), not truncation-toward-zero like SDIV. |
| SUB (0x03) | Negation is implemented as SUB(0, x). The pattern 0 - int256.min overflows (same value, cannot negate), which feeds into SDIV edge cases. |