Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x1C |
| Mnemonic | SHR |
| Gas | 3 |
| Stack Input | shift, value |
| Stack Output | value >> shift (logical) |
| Behavior | Logical right shift. Shifts value right by shift bits, filling vacated high bits with zeros. If shift >= 256, the result is 0. Equivalent to value / 2^shift (floor division) for unsigned values. |
Threat Surface
SHR (logical shift right) was introduced alongside SHL and SAR in the Constantinople hard fork (EIP-145). It performs unsigned right shift, filling vacated high bits with zeros. This makes it equivalent to floor division by a power of 2 for unsigned values — but not for signed values, where SAR (arithmetic shift right) is required to preserve the sign.
SHR’s threat surface spans three main areas:
-
Division truncation:
SHR(n, x)is equivalent tox / 2^nwith floor truncation. Unlike floating-point division, this truncation always rounds toward zero (for unsigned values). When used as a gas-optimized division, the precision loss can be exploited in DeFi protocols that compute prices, fees, or share values. -
Confusion with SAR for signed values: SHR always fills high bits with zeros. For negative values in two’s complement, this produces a positive result (the sign bit is cleared), which is mathematically wrong. SAR preserves the sign bit by filling with ones. Using SHR on signed values is a critical error.
-
Shift >= 256 returns zero: Like SHL, any shift amount >= 256 produces 0 silently. This is the “division by infinity” case.
The combination of truncation and silent zeroing makes SHR dangerous in financial calculations where precision matters. A SHR(1, 3) (equivalent to 3 / 2) returns 1, losing 0.5. Over many operations, this rounding error accumulates and can be exploited by attackers who structure transactions to consistently benefit from the rounding direction.
Smart Contract Threats
T1: Division Truncation Exploitation (High)
SHR-as-division truncates toward zero. In DeFi contexts, this truncation can be exploited:
// Share calculation: how many shares for a deposit?
uint256 shares = (depositAmount << 96) / pricePerShare;
// If pricePerShare involves SHR, truncation favors one direction
// Fee calculation
uint256 fee = amount >> 8; // ~0.39% fee (amount / 256)
// For amount = 255: fee = 0 (not 0.996)
// Attacker makes 255-wei transactions to pay zero feesThe truncation direction is predictable and always rounds down. Attackers who can control the dividend (numerator) can choose values that maximize truncation in their favor:
- Deposit dust amounts: Deposit amounts that truncate to 0 shares, then withdraw using a minimum share value
- Avoid fees: Transaction amounts below the fee threshold produce zero fees
- Accumulate rounding errors: Repeated small operations accumulate truncation in the attacker’s favor
T2: SHR on Signed Values — Sign Bit Destruction (Critical)
SHR always fills high bits with zeros, destroying the sign bit of negative two’s complement values:
int256 negative = -100;
// In two's complement: 0xFFFF...FF9C
// SHR(1, negative) as uint256:
// 0x7FFF...FFCE (large positive number, not -50)
// SAR(1, negative) correctly produces:
// 0xFFFF...FFCE (-50 in two's complement)Using SHR instead of SAR on signed values converts negative numbers to extremely large positive numbers, which can cause:
- Lending protocol interest calculations to overflow
- Oracle price feeds to report astronomically high prices
- Balance calculations to wrap positive
Before Constantinople (when SAR didn’t exist), Solidity emulated signed right shift using a combination of SHR and SIGNEXTEND. The Solidity compiler issues #3847 and #4084 documented bugs where signed right shifts were incorrectly implemented using SDIV (which rounds toward zero) instead of proper SAR (which rounds toward negative infinity for negative values).
T3: Shift >= 256 Returns Zero (High)
Like SHL, SHR(256, x) and any larger shift amount returns 0 silently:
assembly {
let result := shr(shift, value)
// If shift >= 256: result = 0 regardless of value
}When the shift amount is computed from external data (oracle precision, token decimals, time deltas), an attacker who can push the computation above 255 zeroes the result.
T4: Precision Loss in Fixed-Point Downscaling (High)
SHR is used to convert high-precision fixed-point values to lower precision. Common patterns:
// Q96 to integer: divide by 2^96
uint256 integerPrice = sqrtPriceX96 >> 96;
// WAD to integer: this is wrong, WAD uses base-10 scaling
uint256 wrong = wadValue >> 18; // Bug: >> 18 divides by 2^18, not 10^18The confusion between base-2 scaling (powers of 2, using SHR) and base-10 scaling (powers of 10, using DIV) is a recurring error. value >> 18 divides by 262,144, not by 1,000,000,000,000,000,000.
T5: Stack Ordering Confusion (Medium)
Like SHL, SHR takes shift as the first argument: shr(shift, value). Swapping arguments produces wildly wrong results:
// Correct: shift value right by 8 positions
let result := shr(8, value) // value >> 8
// Bug: shift 8 right by 'value' positions
let result := shr(value, 8) // 8 >> value (likely 0 for most values)Protocol-Level Threats
P1: No DoS Vector (Low)
SHR costs a fixed 3 gas regardless of operand values.
P2: Consensus Safety (Low)
SHR is trivially deterministic. All EVM implementations agree on its behavior. The EIP-145 specification is clear and well-tested.
P3: Rounding Direction Differs from SDIV (Low)
SHR rounds toward zero for unsigned values (SHR(1, 3) == 1). SDIV also rounds toward zero for signed values (SDIV(-3, 2) == -1). But SAR rounds toward negative infinity (SAR(1, -3) == -2). This three-way difference in rounding is specified correctly but can cause consensus bugs in alternative EVM implementations that use the wrong rounding mode.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
SHR(0, x) | Returns x | No shift; identity |
SHR(1, x) | Returns x / 2 (floor) | Truncation: SHR(1, 3) == 1, not 1.5 |
SHR(255, x) | Returns 0 or 1 | Only the MSB survives; effective sign-bit check for unsigned |
SHR(256, x) | Returns 0 | Complete information loss; silent |
SHR(MAX_UINT256, x) | Returns 0 | Any shift >= 256 returns 0 |
SHR(n, 0) | Returns 0 | Zero shifted by any amount is zero |
SHR(1, 1) | Returns 0 | 1 / 2 = 0 with floor truncation |
SHR(n, MAX_UINT256) | Returns 2^(256-n) - 1 | All n low bits removed, upper bits preserved |
| SHR on two’s complement negative | High bits filled with 0, not 1 | Converts negative to large positive; use SAR instead |
Real-World Exploits
Exploit 1: Solidity Signed Right Shift Rounding Bug (Issue #3847, 2018)
Root cause: Before Constantinople, Solidity emulated signed right shift using SDIV. SDIV rounds toward zero, while proper arithmetic right shift (SAR) rounds toward negative infinity. This produced incorrect results for negative odd values.
Details: The Solidity compiler emulated int256 x >> n as x / 2^n using SDIV. For negative values, SDIV rounds toward zero: SDIV(-3, 2) == -1. But SAR rounds toward negative infinity: SAR(1, -3) == -2. The difference matters for any negative odd value shifted right.
The fix (Solidity PR #4084) was a breaking change: use proper SAR instruction on Constantinople+ targets, and emulate SAR correctly on pre-Constantinople targets using SHR combined with SIGNEXTEND.
SHR’s role: SHR was part of the pre-Constantinople emulation of SAR. The emulation used SHR(shift, value) followed by sign extension via SIGNEXTEND. Getting this emulation wrong (or using SHR directly without sign extension) was the root cause of incorrect signed shift behavior.
Impact: Subtle rounding errors in contracts using signed right shift. Particularly dangerous for financial calculations involving negative values (e.g., negative interest rate adjustments, negative price deltas).
References:
- Solidity Issue #3847: Signed right shifts use SDIV and round differently
- Solidity PR #4084: Use proper SAR for signed right shifts
Exploit 2: Rounding-Direction Exploits in DeFi Share Calculations (Recurring Pattern)
Root cause: Using SHR (floor-toward-zero division) in share/token calculations where rounding should favor the protocol, not the user.
Details: DeFi vault contracts compute shares as shares = (deposit * totalShares) / totalAssets. When division is implemented via SHR for power-of-2 denominators, truncation always rounds down. Depending on context, this can favor either the depositor or the vault:
- Deposit: Rounding down shares favors the vault (depositor gets fewer shares) — usually safe
- Withdrawal: Rounding down assets favors the withdrawer (vault pays less) — can be exploited
The ERC-4626 tokenized vault standard explicitly recommends different rounding directions for deposits (round down) vs withdrawals (round up). Using SHR uniformly applies floor rounding in both directions, which is incorrect for withdrawals.
SHR’s role: SHR always rounds toward zero (floor division). There is no “round up” shift instruction. To round up, contracts must use (x + (1 << n) - 1) >> n or equivalent, which adds complexity and gas.
Impact: The OWASP Smart Contract Top 10 (2026) lists arithmetic/rounding errors as SC07, noting that incorrect rounding has led to exploitable share inflation in multiple DeFi protocols. The Yearn Finance yETH exploit ($9M, November 2025) involved rounding-down failures in the invariant solver that zeroed critical product terms.
References:
Exploit 3: Base-2 vs Base-10 Scaling Confusion (Recurring Pattern)
Root cause: Using SHR (divides by power of 2) when DIV by power of 10 was intended, or vice versa.
Details: Ethereum’s standard unit scaling uses powers of 10 (1 ETH = 10^18 wei, fee rates as basis points = 1/10000). SHR divides by powers of 2. Confusing the two produces dramatically wrong results:
| Operation | SHR Equivalent | DIV Equivalent | Ratio |
|---|---|---|---|
| ”Divide by 1000” | >> 10 (÷ 1024) | / 1000 | 1.024x error |
| ”Divide by 1e6” | >> 20 (÷ 1048576) | / 1000000 | 1.049x error |
| ”Divide by 1e18” | >> 60 (÷ 1.15e18) | / 1e18 | 1.15x error |
A 15% pricing error (>> 60 vs / 1e18) in an AMM or lending protocol is catastrophically exploitable. Attackers who notice the discrepancy can arbitrage the mispriced assets.
SHR’s role: SHR provides division by powers of 2, which don’t align with Ethereum’s base-10 denomination. The optimization temptation (“SHR is cheaper than DIV”) can lead developers to use approximate power-of-2 divisors.
Attack Scenarios
Scenario A: Fee Evasion via Truncation
contract VulnerableExchange {
function swap(uint256 amountIn) external returns (uint256 amountOut) {
// 0.3% fee implemented as: amount * 3 / 1000
// Gas-optimized (incorrectly) as: amount >> 8 (~0.39%)
// But the real bug: for amountIn < 256, fee truncates to 0
uint256 fee = amountIn >> 8;
amountOut = computeOutput(amountIn - fee);
}
}Attack: Make many swaps of 255 wei each. Fee is 0 on each. Accumulate zero-fee trades to extract value.
Scenario B: Signed Value Corruption
contract VulnerableOracle {
function adjustPrice(int256 price, uint256 precision) external pure returns (int256) {
int256 adjusted;
assembly {
// Bug: SHR destroys sign bit for negative prices
adjusted := shr(precision, price)
// If price = -1000 (0xFFFF...FC18):
// SHR(1) = 0x7FFF...FE0C (huge positive number!)
}
return adjusted;
}
// Correct: use SAR for signed values
// adjusted := sar(precision, price)
}Attack: Submit a negative price (e.g., from a negative interest rate oracle). SHR converts it to a huge positive number. Downstream calculations produce astronomical values.
Scenario C: Base-2 vs Base-10 Scaling Error
contract VulnerableLending {
uint256 constant PRECISION = 18; // Intended: 10^18
function toBaseUnits(uint256 amount) internal pure returns (uint256) {
// Bug: >> 18 divides by 2^18 = 262144, not 10^18
return amount >> PRECISION;
// 1 ETH (10^18 wei) >> 18 = 3814697265625 (not 1!)
}
}Scenario D: Shift Overflow Zeroing in Price Feed
contract PriceFeed {
function normalizeDecimals(uint256 price, uint256 fromDecimals, uint256 toDecimals) external pure returns (uint256) {
if (fromDecimals > toDecimals) {
uint256 diff = fromDecimals - toDecimals;
// Bug: if diff is very large (e.g., fromDecimals=1000 due to bad data),
// shift >= 256 and price becomes 0
return price >> (diff * 4); // Approximate: divide by 16^diff
}
return price << ((toDecimals - fromDecimals) * 4);
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Truncation exploitation | Round against the user/attacker (pro-protocol rounding) | Use (x + (1 << n) - 1) >> n for round-up SHR |
| T1: Fee evasion | Enforce minimum transaction amounts or minimum fees | require(amount >= MIN_AMOUNT) or fee = max(minFee, amount >> 8) |
| T2: Signed value corruption | Never use SHR on signed values; use SAR | sar(shift, value) in assembly; >> on int256 in Solidity uses SAR automatically |
| T2: Type confusion | Use Solidity’s type system; int256 >> n uses SAR automatically | Avoid assembly for signed arithmetic |
| T3: Shift >= 256 | Validate shift amount before SHR | require(shift < 256) |
| T4: Fixed-point precision | Use established math libraries (FixedPointMathLib, PRBMath) | FullMath.mulDiv() for precision-safe division |
| T4: Base-2 vs base-10 | Never use SHR as an approximation for base-10 division | Use / 10**n for base-10; only use >> n when dividing by 2^n is the actual intent |
| T5: Stack ordering | Use Solidity >> operator instead of assembly shr() | Solidity handles argument ordering correctly |
Rounding Best Practices
// Round DOWN (toward zero) -- favors protocol on deposits
function divDown(uint256 x, uint256 y) internal pure returns (uint256) {
return x / y; // or x >> n for power-of-2 y
}
// Round UP (away from zero) -- favors protocol on withdrawals
function divUp(uint256 x, uint256 y) internal pure returns (uint256) {
return (x + y - 1) / y; // or (x + (1 << n) - 1) >> n
}
// ERC-4626 standard: round down on deposit, round up on withdrawSeverity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | High | Rounding exploits in DeFi vaults (OWASP SC07) |
| T2 | Smart Contract | Critical | Medium | Solidity signed shift bugs (#3847, #4084) |
| T3 | Smart Contract | High | Medium | Shift overflow zeroing in price feeds |
| T4 | Smart Contract | High | Medium | Base-2/base-10 confusion in scaling |
| T5 | Smart Contract | Medium | Low | Assembly stack ordering errors |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
| P3 | Protocol | Low | Low | Rounding mode implementation differences |
Related Opcodes
| Opcode | Relationship |
|---|---|
| SHL (0x1B) | Inverse operation; SHL and SHR undo each other (with bit loss). SHL(n, SHR(n, x)) clears the lowest n bits |
| SAR (0x1D) | Arithmetic right shift; preserves sign. Use SAR instead of SHR for signed values |
| DIV (0x04) | General division; SHR is only equivalent for power-of-2 divisors. DIV costs 5 gas vs SHR’s 3 |
| SDIV (0x05) | Signed division rounds toward zero; SAR rounds toward negative infinity. Different from both SHR and SAR |
| AND (0x16) | SHR(n, x) followed by AND(result, mask) extracts a field from a packed value |
| BYTE (0x1A) | SHR(248, x) is equivalent to BYTE(0, x) for extracting the MSB |
| SIGNEXTEND (0x0B) | Used in pre-Constantinople SAR emulation: SHR then SIGNEXTEND to restore sign bits |