Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x1D |
| Mnemonic | SAR |
| Gas | 3 |
| Stack Input | shift, value |
| Stack Output | value >> shift (arithmetic, sign-preserving) |
| Behavior | Arithmetic right shift. Shifts value right by shift bits, filling vacated high bits with copies of the sign bit (bit 255). If shift >= 256, the result is 0 if value is non-negative (bit 255 = 0), or MAX_UINT256 (-1) if value is negative (bit 255 = 1). |
Threat Surface
SAR (Shift Arithmetic Right) is the EVM’s only sign-preserving shift operation, introduced in Constantinople (EIP-145) alongside SHL and SHR. Its defining property is sign extension: when shifting right, vacated high bits are filled with copies of the sign bit (bit 255), not zeros. This preserves the sign of two’s complement values:
SAR(1, -100)→-50(high bits filled with 1s, sign preserved)SHR(1, -100)→ huge positive number (high bits filled with 0s, sign destroyed)
SAR’s threat surface is dominated by three issues:
-
Using SAR on unsigned values: SAR treats all values as signed. If an unsigned value happens to have bit 255 set (i.e., >= 2^255), SAR treats it as negative and fills high bits with 1s, producing a result near MAX_UINT256 instead of the expected small positive number.
-
Rounding toward negative infinity: SAR rounds differently from SDIV.
SAR(1, -3)= -2 (floor toward negative infinity), whileSDIV(-3, 2)= -1 (truncation toward zero). This difference has caused compiler bugs and can lead to exploitable discrepancies in financial calculations. -
Sign extension on shift >= 256: Unlike SHR (which always returns 0 for shift >= 256), SAR returns 0 for non-negative values but
MAX_UINT256(-1 in two’s complement) for negative values. This asymmetry can be exploited when the shift amount is computed and exceeds 255.
The historical context is important: before EIP-145 (Constantinople, February 2019), there was no SAR instruction. Solidity emulated signed right shift using SDIV, which has different rounding behavior. The transition from SDIV-emulated shifts to native SAR was a breaking change (Solidity PR #4084) that affected contracts relying on the SDIV rounding direction.
Smart Contract Threats
T1: SAR on Unsigned Values — False Negative Interpretation (Critical)
The most dangerous SAR misuse: applying it to values that are logically unsigned but numerically have bit 255 set. In uint256, values >= 2^255 are perfectly valid large positive numbers. But SAR interprets bit 255 as the sign bit, treating these values as negative:
assembly {
let x := 0x8000000000000000000000000000000000000000000000000000000000000001
// As uint256: ~5.79 * 10^76 (large but valid)
// SAR interpretation: negative (bit 255 = 1)
let result := sar(1, x)
// Returns: 0xC000000000000000000000000000000000000000000000000000000000000000
// High bits filled with 1s (sign extension)!
// As uint256: ~8.68 * 10^76 (LARGER than input -- completely wrong for unsigned division)
// SHR would correctly return:
// 0x4000000000000000000000000000000000000000000000000000000000000000
// ~2.89 * 10^76 (half the input)
}This can occur in DeFi when token amounts, prices, or hash values exceed 2^255. While uncommon for single-token balances, it easily happens in intermediate calculations (e.g., price * amount producing a value > 2^255).
T2: Rounding Direction Differs from SDIV and SHR (High)
The three right-shift-equivalent operations in the EVM round differently:
| Operation | (-3) shifted right by 1 | Rounding Direction |
|---|---|---|
SAR(1, -3) | -2 | Toward negative infinity (floor) |
SDIV(-3, 2) | -1 | Toward zero (truncation) |
SHR(1, -3 as uint256) | Huge positive | Wrong for signed |
The SAR vs SDIV difference means that int256(-3) >> 1 and int256(-3) / 2 produce different results. This was the root cause of Solidity Issue #3847: before Constantinople, the compiler used SDIV for signed right shifts, producing -1 instead of the mathematically correct -2.
In financial contexts, this rounding difference matters:
- Interest calculations: Negative interest (penalty) that rounds toward zero (SDIV) is smaller than one that rounds toward negative infinity (SAR)
- Price adjustments: Downward price corrections rounded differently produce systematically different valuations
- Reward distribution: Negative adjustments rounded toward zero give users more than they’re owed
T3: Sign Extension on Shift >= 256 (High)
SAR has asymmetric behavior for large shift amounts:
| Input | SAR(256, input) | Explanation |
|---|---|---|
| Any non-negative (bit 255 = 0) | 0 | Same as SHR |
| Any negative (bit 255 = 1) | MAX_UINT256 (-1) | Sign extension fills all 256 bits with 1 |
If the shift amount is user-controlled or computed from external data and can reach 256+, a negative input produces MAX_UINT256 instead of 0. When interpreted as uint256, this is the maximum possible value:
assembly {
// Shift amount from external source
let result := sar(300, 0x8000000000000000000000000000000000000000000000000000000000000001)
// result = MAX_UINT256 (all bits set)
// As uint256: ~1.16 * 10^77
}T4: Pre-Constantinople Emulation Correctness (Medium)
Contracts compiled for pre-Constantinople targets (or using older Solidity versions that emulate SAR) may have incorrect signed shift behavior. The emulation using SDIV rounds toward zero instead of toward negative infinity. Contracts relying on the exact rounding behavior of SAR but compiled with SDIV emulation produce different results for negative odd values.
Solidity fixed this in PR #4084, implementing proper SAR emulation on pre-Constantinople targets using SHR + SIGNEXTEND. However, contracts compiled with older compilers retain the buggy SDIV-based emulation.
T5: Mixing SAR with Unsigned Comparisons (Medium)
After a SAR operation on a negative value, the result is still negative (in two’s complement). If this result is then compared using unsigned operations (LT, GT) instead of signed operations (SLT, SGT), the negative value appears as a huge positive number:
assembly {
let shifted := sar(1, sub(0, 100)) // SAR(1, -100) = -50
// shifted = 0xFFFF...FFCE (-50 in two's complement)
let isSmall := lt(shifted, 100) // Unsigned: 0xFFFF...FFCE < 100? No!
// isSmall = 0 (false) -- wrong!
let isSmallSigned := slt(shifted, 100) // Signed: -50 < 100? Yes!
// isSmallSigned = 1 (true) -- correct
}Protocol-Level Threats
P1: No DoS Vector (Low)
SAR costs a fixed 3 gas regardless of operand values.
P2: Consensus Safety — Rounding Specification (Medium)
SAR’s rounding behavior (toward negative infinity) is precisely specified in EIP-145. However, alternative EVM implementations that derive SAR from SDIV (which rounds toward zero) will produce different results for negative odd values. This is a potential consensus divergence vector for new EVM implementations.
The EIP-145 specification is explicit: SAR performs arithmetic right shift with sign extension, equivalent to floor(value / 2^shift) where floor rounds toward negative infinity. Implementations that use truncation toward zero are non-compliant.
P3: No State Impact (None)
SAR modifies only the stack.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
SAR(0, x) | Returns x | No shift; identity |
SAR(1, -1) | Returns -1 (MAX_UINT256) | -1 >> any amount stays -1 (all bits are 1) |
SAR(1, -2) | Returns -1 (MAX_UINT256) | -2 / 2 rounds to -1 (toward negative infinity) |
SAR(1, -3) | Returns -2 | Floor division: -3/2 = -2, not -1 |
SAR(255, -1) | Returns -1 (MAX_UINT256) | Sign extends to fill all 256 bits |
SAR(256, positive) | Returns 0 | Same as SHR for non-negative |
SAR(256, negative) | Returns MAX_UINT256 (-1) | Sign extension: all bits become 1 |
SAR(MAX_UINT256, 1) | Returns 0 | Positive value, massive shift |
SAR(MAX_UINT256, -1) | Returns MAX_UINT256 (-1) | Negative, massive shift = all sign bits |
SAR(1, 0) | Returns 0 | Zero is non-negative; high bits filled with 0 |
SAR(n, uint > 2^255) | Sign-extends as negative | Unsigned values with MSB set are treated as negative |
SAR(1, MIN_INT256) | Returns 0xC000...0000 (-2^254) | Correct: -2^255 / 2 = -2^254 |
Real-World Exploits
Exploit 1: Solidity SDIV/SAR Rounding Discrepancy (Issue #3847 / PR #4084, 2018)
Root cause: Solidity used SDIV to emulate signed right shift, but SDIV rounds toward zero while SAR rounds toward negative infinity. This produced incorrect results for negative odd values.
Details: Before the Constantinople hard fork introduced native SAR, Solidity compiled int256 x >> n as x / 2^n using SDIV. The rounding difference:
SAR(1, -3)=-2(floor toward negative infinity: -3/2 = -1.5, floor = -2)SDIV(-3, 2)=-1(truncation toward zero: -3/2 = -1.5, truncate = -1)
This was a documented breaking change when Solidity switched to native SAR (PR #4084). Contracts that depended on the SDIV rounding behavior (truncation toward zero) would produce different results after the compiler update.
SAR’s role: SAR’s rounding toward negative infinity is the mathematically correct behavior for arithmetic right shift. The pre-SAR emulation was wrong, and fixing it changed program semantics.
Impact: Subtle rounding changes in signed arithmetic for all contracts recompiled with the fix. Particularly relevant for financial calculations involving negative interest rates, price deltas, or penalty computations.
References:
- Solidity Issue #3847: Signed right shifts use SDIV
- Solidity PR #4084: Use proper SAR for signed right shifts
- EIP-145: Bitwise shifting instructions in EVM
Exploit 2: Disallowing Shift by Signed Types (Solidity Issue #8822)
Root cause: Allowing signed types as the shift amount could produce negative shift values, which have undefined or confusing semantics in the EVM.
Details: Solidity Issue #8822 identified that using signed integers as shift amounts (e.g., uint256 result = x >> int256(-1)) could lead to confusion and bugs. A negative shift amount, when interpreted as uint256, becomes an astronomically large number (>= 256), causing SAR to return either 0 or MAX_UINT256 depending on the sign of the shifted value.
Solidity resolved this by disallowing signed types as the shift amount at the compiler level. The shift amount must always be unsigned, preventing accidental interpretation of negative values as huge shifts.
SAR’s role: SAR’s asymmetric behavior for shift >= 256 (returns 0 for positive, MAX_UINT256 for negative) makes negative shift amounts especially dangerous — the result depends not just on the magnitude of the shift but also on the sign of the value being shifted.
References:
Exploit 3: Rounding-Direction Exploitation in DeFi Negative Adjustments (Recurring Pattern)
Root cause: Financial calculations using SAR-based division on negative values round differently from SDIV-based division, creating exploitable discrepancies.
Details: DeFi protocols that handle negative adjustments (slashing penalties, negative rebasing, fee deductions) must carefully choose their rounding direction. SAR rounds toward negative infinity (making penalties larger in absolute terms), while SDIV rounds toward zero (making penalties smaller).
The OWASP Smart Contract Security Top 10 (2026) identifies arithmetic/rounding errors (SC07) as a critical vulnerability class, noting that rounding inconsistencies between deposits and withdrawals have been exploited in multiple protocols. When protocols use SAR for some calculations and SDIV for others on the same negative values, the inconsistency creates extractable value.
For example, a lending protocol that calculates interest using SAR-rounding but applies rebates using SDIV-rounding creates a systematic discrepancy: borrowers pay more interest (SAR rounds the negative rate floor-ward, making the absolute deduction larger) but receive less in rebates (SDIV rounds toward zero, making the rebate smaller).
SAR’s role: SAR’s rounding direction is mathematically correct but differs from what many developers expect (truncation toward zero). The mismatch between expectation and behavior creates bugs when SAR and SDIV are mixed.
References:
Attack Scenarios
Scenario A: SAR on Unsigned Value > 2^255
contract VulnerablePool {
function computeHalfPrice(uint256 sqrtPriceX96) external pure returns (uint256) {
uint256 halfPrice;
assembly {
// Bug: sar instead of shr on unsigned value
halfPrice := sar(1, sqrtPriceX96)
// If sqrtPriceX96 >= 2^255 (possible with very high prices):
// SAR treats it as negative, sign-extends, produces ~MAX_UINT256/2
// instead of sqrtPriceX96/2
}
return halfPrice;
}
}Attack: Manipulate pool price above 2^255 (via flash loan). SAR produces an enormous number instead of half the price. Use the inflated value to extract liquidity.
Scenario B: Rounding Direction Exploitation
contract LendingPool {
int256 public negativeInterestRate = -3; // -3 basis points per period
function computeAdjustment(uint256 principal) external view returns (int256) {
int256 signedPrincipal = int256(principal);
int256 adjustment;
assembly {
// SAR: -3 >> 1 = -2 (floor division, larger absolute penalty)
// If SDIV were used: -3 / 2 = -1 (truncation, smaller penalty)
adjustment := sar(1, mul(signedPrincipal, sload(negativeInterestRate.slot)))
}
return adjustment;
}
}Attack: If the protocol inconsistently uses SAR in some paths and SDIV in others, arbitrage the rounding difference across the two paths.
Scenario C: Sign Extension on Large Shift
contract VulnerableScaler {
function scaleDown(int256 value, uint256 decimals) external pure returns (int256) {
int256 result;
assembly {
// Bug: if decimals > 77 (since 4*78 > 256), shift >= 256
// For negative value: SAR returns MAX_UINT256 (-1)
// For positive value: SAR returns 0
result := sar(mul(decimals, 4), value)
}
return result;
// With decimals = 100 and value = -5:
// shift = 400 >= 256, result = MAX_UINT256 = -1 (not 0!)
}
}Scenario D: Mixing SAR Result with Unsigned Comparison
contract VulnerableCheck {
function isSmallAdjustment(int256 adjustment) external pure returns (bool) {
uint256 absAdj;
assembly {
// Compute absolute value of adjustment
let mask := sar(255, adjustment) // All 1s if negative, all 0s if positive
absAdj := xor(add(adjustment, mask), mask) // XOR trick for abs
}
// Bug: if the SAR + XOR + ADD chain has an error,
// absAdj could be wrong
return absAdj < 1000;
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: SAR on unsigned values | Never use SAR on uint256; use SHR | Code review: flag sar() in assembly that operates on unsigned variables |
| T1: Type enforcement | Use Solidity’s >> operator (automatically selects SAR for int, SHR for uint) | Avoid assembly for shift operations when possible |
| T2: Rounding direction | Document intended rounding; use consistent division method | Choose SAR or SDIV, never mix for the same calculation |
| T2: Financial rounding | Round against the user (pro-protocol) | For penalties: SAR (larger absolute deduction). For rebates: SDIV (smaller rebate) |
| T3: Sign extension overflow | Validate shift amount < 256 | require(shift < 256) or handle >= 256 case explicitly |
| T3: Asymmetric result | Test with both positive and negative values at shift boundaries | Fuzz: (positive, 256), (negative, 256), (positive, 255), (negative, 255) |
| T4: Pre-Constantinople compat | Recompile with modern Solidity; verify behavior matches SAR semantics | Test signed right shift with negative odd values: -3, -5, -7 |
| T5: Unsigned comparison | Use SLT/SGT after SAR, not LT/GT | slt(sarResult, threshold) instead of lt(sarResult, threshold) |
Key Decision: SAR vs SHR vs SDIV
| When to use… | Operation | Rounding | Sign handling |
|---|---|---|---|
| SHR | Unsigned division by 2^n | Toward zero (floor) | Unsigned only; destroys sign |
| SAR | Signed division by 2^n | Toward negative infinity | Preserves sign via extension |
| SDIV | Signed division by any value | Toward zero (truncation) | Preserves sign via result sign |
| DIV | Unsigned division by any value | Toward zero (floor) | Unsigned only |
Use SHR for uint256, SAR for int256, and never mix them. Let the Solidity compiler choose the right instruction via the >> operator on the appropriate type.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Medium | SAR on large unsigned values in DeFi |
| T2 | Smart Contract | High | Medium | Solidity SDIV/SAR rounding discrepancy (#3847) |
| T3 | Smart Contract | High | Low | Sign extension on large shift amounts |
| T4 | Smart Contract | Medium | Low | Pre-Constantinople emulation bugs |
| T5 | Smart Contract | Medium | Medium | Unsigned comparison after SAR |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Medium | Low | EVM implementation rounding disagreements |
Related Opcodes
| Opcode | Relationship |
|---|---|
| SHR (0x1C) | Logical right shift (unsigned). Fills high bits with 0, not sign bit. Use SHR for uint256, SAR for int256 |
| SHL (0x1B) | Left shift; no signed variant exists. SHL is used for both signed and unsigned left shifts |
| SDIV (0x05) | Signed division; rounds toward zero (truncation). Different rounding from SAR for negative odd values |
| DIV (0x04) | Unsigned division; SAR on values with bit 255 = 0 is equivalent to DIV by power of 2 |
| SIGNEXTEND (0x0B) | Sign-extends a sub-256-bit value. Used in pre-Constantinople SAR emulation and for correcting dirty sign bits |
| SLT (0x12) | Signed less-than comparison. Must use SLT (not LT) after SAR to correctly compare signed results |
| SGT (0x13) | Signed greater-than comparison. Must use SGT (not GT) after SAR for correct comparison |
| ISZERO (0x15) | Used to check if SAR result is zero; works correctly regardless of sign interpretation |