Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x17 |
| Mnemonic | OR |
| Gas | 3 |
| Stack Input | a, b |
| Stack Output | a | b |
| Behavior | Bitwise OR of two 256-bit values. Each bit of the result is 1 if the corresponding bit of either operand is 1. |
Threat Surface
OR is the EVM’s primary bit-setting and value-combining opcode. Where AND extracts and masks, OR assembles and merges. Its core use cases are:
- Packed storage writes: Combining multiple fields into a single uint256 slot (
(field1 << 160) | field2) - Flag setting: Enabling permission bits in role bitmaps (
permissions | NEW_FLAG) - Bitfield construction: Building composite values from individual components
- Compiler-generated overflow checks: OR appears in Solidity’s compiled overflow detection patterns
The threat surface of OR is more subtle than AND. OR errors rarely cause immediate reverts or obvious failures — they silently add bits that shouldn’t be there, or combine values with overlapping bit ranges, producing composite values that look valid but contain corrupted data. This “silent corruption” property makes OR bugs particularly dangerous in packed storage patterns where multiple economic values share a single storage slot.
Unlike arithmetic opcodes where overflow produces dramatically wrong values, OR errors produce values within the expected range but with wrong bits set. A single wrong bit in a packed slot can reassign ownership, flip permission flags, or corrupt balance fields — all while the overall value appears “normal.”
Smart Contract Threats
T1: Packed Value Overlap / Clobbering (Critical)
When packing multiple values into a single uint256 via OR, the fields must occupy non-overlapping bit ranges. If two fields overlap even by a single bit, OR merges their values, corrupting both. This occurs when:
- Shift amounts are calculated incorrectly (off-by-one in bit position)
- A field width exceeds its allocated range (e.g., a uint96 value stored in a 88-bit slot)
- Fields are combined without first clearing the target bits via AND
// Packed slot: [96 bits: balance][160 bits: owner address]
function setOwnerAndBalance(uint256 slot, address owner, uint96 balance) internal pure returns (uint256) {
// Bug: balance shifted by 159 instead of 160 -- overlaps owner's top bit
return (uint256(balance) << 159) | uint256(uint160(owner));
// The lowest bit of balance clobbers the highest bit of the owner address
}Why it matters: Packed storage is extremely common in gas-optimized DeFi protocols (Uniswap v3/v4, Compound, Aave). A single-bit overlap in a pool’s packed state can corrupt prices, liquidity positions, or ownership records.
T2: Missing Field Clear Before OR Update (High)
When updating a single field in a packed slot, the old field value must be cleared (via AND with inverted mask) before OR-ing in the new value. If the clear step is missing, the old and new values are OR’d together, merging their bits:
// Correct: clear then set
packed = (packed & ~BALANCE_MASK) | (uint256(newBalance) << BALANCE_OFFSET);
// Bug: OR without clearing -- old balance bits merge with new
packed = packed | (uint256(newBalance) << BALANCE_OFFSET);
// If old balance had bits set that new balance doesn't, they persistThis bug is especially dangerous because it only manifests when the new value has fewer bits set than the old value (i.e., the new value is smaller in certain bit positions). Setting a balance from 0xFF to 0x0F works correctly; setting from 0xFF to 0xF0 also appears to work (result is 0xFF, not 0xF0). The balance can only increase, never decrease.
T3: Flag Accumulation Without Revocation (High)
Permission systems that use OR to add flags but lack a corresponding AND-NOT revocation mechanism create irrevocable permissions:
function grantRole(address user, uint256 role) external onlyAdmin {
userRoles[user] = userRoles[user] | role; // Adds bits
}
// Missing: revokeRole that clears bits via AND
// function revokeRole(address user, uint256 role) external onlyAdmin {
// userRoles[user] = userRoles[user] & ~role; // Clears bits
// }If the revocation function is missing, permissions can only be granted, never removed. A compromised admin who grants themselves all roles before being “demoted” retains all permissions.
T4: Incorrect Boolean OR vs Bitwise OR (Medium)
In Solidity, || is logical OR (short-circuiting, returns bool) while | is bitwise OR (evaluates both operands, returns uint). Confusion between the two in inline assembly or unchecked contexts can cause:
- Both sides of a conditional being evaluated (side effects in the “false” branch execute)
- Numeric values being OR’d instead of boolean values, producing unexpected non-zero results
T5: OR in Compiler-Generated Code (Low)
The Solidity 0.8+ compiler uses OR in overflow detection patterns (e.g., ISZERO(a) OR (DIV(MUL(a,b), a) == b) for MUL overflow). While not directly exploitable, auditors reviewing bytecode should understand that OR operations in compiled code may be part of safety checks rather than application logic.
Protocol-Level Threats
P1: No DoS Vector (Low)
OR costs a fixed 3 gas with no dynamic component. It operates purely on the stack and cannot be used for gas griefing.
P2: Consensus Safety (Low)
OR is trivially deterministic: each output bit is the logical OR of the corresponding input bits. No EVM implementation disagreements have occurred due to OR.
P3: No State Impact (None)
OR modifies only the stack. It cannot cause state bloat, storage writes, or memory expansion.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
x | 0 | Returns x | Identity; OR with zero has no effect |
x | MAX_UINT256 | Returns MAX_UINT256 | All bits set regardless of x; potential saturation bug |
x | x | Returns x | Idempotent; no issue |
x | ~x | Returns MAX_UINT256 | Value and complement cover all bits |
| OR of overlapping fields | Bits from both fields merge | Silent data corruption in packed slots |
| OR of identical field positions | Higher value dominates | Cannot distinguish which field contributed which bits |
0 | 0 | Returns 0 | No issue |
| Large value OR small value | Small value’s bits absorbed | May appear to “work” but corrupts field boundaries |
Real-World Exploits
Exploit 1: Packed Storage Corruption in DeFi Protocols (Ongoing Pattern)
Root cause: Incorrect use of OR when updating packed storage slots, failing to clear existing bits before writing new values.
Details: Multiple DeFi protocols have experienced bugs related to packed storage updates where OR was used without first clearing the target field. This pattern is especially common in gas-optimized protocols that pack balances, timestamps, and flags into single storage slots to save SSTORE costs. The canonical pattern appears in Uniswap v3’s Position struct and similar designs.
While no single exploit of this pattern has resulted in catastrophic loss, audit firms routinely flag this as a high-severity finding. Trail of Bits, OpenZeppelin, and Consensys Diligence have all documented instances in private audits where packed slot OR operations were missing the clear step.
OR’s role: OR is the direct cause — it merges bits instead of replacing them. The fix is always to AND-clear the target field first, then OR in the new value.
Exploit 2: ERC-2771 Context Corruption via Packed Calldata (December 2023)
Root cause: In the Time token exploit ($190K), the trusted forwarder pattern appended the sender address to calldata. When combined with Multicall, the packed calldata structure was corrupted because multiple layers of address appending were OR’d/concatenated without proper boundary tracking.
Details: The ERC-2771 standard packs the original msg.sender address at the end of calldata. When a Multicall operation wraps multiple ERC-2771 calls, the calldata from inner calls can bleed into the sender address extraction region. The resulting address is effectively the OR of legitimate and attacker-controlled bytes (since concatenation at the byte level produces the same effect as OR for non-overlapping positions).
OR’s role: While not a direct use of the OR opcode, the exploit demonstrates the fundamental OR-pattern danger: combining values into a shared space without proper isolation produces corrupted composites.
Impact: ~$190,000 stolen from the Time token on Ethereum. Multiple protocols using ERC-2771 + Multicall were affected.
References:
Attack Scenarios
Scenario A: Packed Balance Can Only Increase
contract VulnerableVault {
// Packed: [96 bits: balance][160 bits: depositor]
mapping(uint256 => uint256) internal _positions;
uint256 constant BALANCE_OFFSET = 160;
function updateBalance(uint256 posId, uint96 newBalance) internal {
// Bug: missing clear step -- old balance bits persist
_positions[posId] = _positions[posId] | (uint256(newBalance) << BALANCE_OFFSET);
// If oldBalance = 0xFF (255), newBalance = 0x01 (1):
// Result balance = 0xFF | 0x01 = 0xFF (255), not 1
// Balance can never decrease via this function!
}
// Correct implementation:
// uint256 constant BALANCE_MASK = uint256(type(uint96).max) << BALANCE_OFFSET;
// _positions[posId] = (_positions[posId] & ~BALANCE_MASK) | (uint256(newBalance) << BALANCE_OFFSET);
}Attack: Deposit 1 wei, then withdraw. Balance update tries to set balance to 0, but OR preserves all previously set bits. Balance appears unchanged. Withdraw again — infinite withdrawals.
Scenario B: Permission Escalation via Missing Revocation
contract BrokenACL {
mapping(address => uint256) public permissions;
uint256 constant TRANSFER = 1 << 0;
uint256 constant MINT = 1 << 1;
uint256 constant ADMIN = 1 << 2;
function grant(address user, uint256 perm) external {
require(permissions[msg.sender] & ADMIN != 0, "not admin");
permissions[user] = permissions[user] | perm;
}
function revoke(address user, uint256 perm) external {
require(permissions[msg.sender] & ADMIN != 0, "not admin");
// Bug: OR instead of AND-NOT -- this ADDS the permission instead of removing it
permissions[user] = permissions[user] | ~perm;
// ~MINT = 0xFFFF...FFFD, so this sets ALL bits except MINT
// User now has ADMIN + TRANSFER + all other bits set!
}
}Attack: Admin tries to revoke MINT from a user. The buggy revocation grants the user every other permission including ADMIN.
Scenario C: Overlapping Field Corruption
contract PackedPool {
// Intended layout: [128 bits: sqrtPrice][64 bits: tick][64 bits: liquidity]
uint256 public poolState;
function updatePrice(uint128 newSqrtPrice) external {
// Bug: shift is 127, not 128 -- price overlaps tick by 1 bit
poolState = (poolState & ~(uint256(type(uint128).max) << 127))
| (uint256(newSqrtPrice) << 127);
// Lowest bit of price clobbers highest bit of tick
// Tick can flip between positive and negative unexpectedly
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Field overlap | Define field boundaries as constants; verify they don’t overlap | assert(FIELD_A_END <= FIELD_B_START) in tests |
| T1: Clobbering | Use struct packing via Solidity compiler instead of manual OR | Let struct { uint96 balance; address owner; } handle layout |
| T2: Missing clear | Always AND-clear before OR-set for packed updates | slot = (slot & ~MASK) | (newValue << OFFSET) |
| T2: One-directional updates | Fuzz-test with decreasing values, not just increasing | Test transitions: large→small, all-ones→all-zeros |
| T3: Missing revocation | Implement AND-NOT revocation for every OR-grant | permissions &= ~role for revocation |
| T4: Boolean confusion | Use bool type and || for boolean logic; uint and | for bitwise | Linter rules to flag | on boolean-like variables |
Best Practices for Packed Storage
- Use libraries: OpenZeppelin’s
BitMapsandSlotDerivationprovide tested bit manipulation. - Define layouts declaratively: Document bit ranges with constants:
uint256 constant BALANCE_OFFSET = 160; uint256 constant BALANCE_WIDTH = 96; - Test at boundaries: Fuzz with values at 2^n-1 and 2^n for each field to detect overlap.
- Prefer compiler packing: Solidity structs automatically pack fields. Manual packing via OR should be reserved for proven hot paths.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Medium | Packed storage bugs in DeFi protocols |
| T2 | Smart Contract | High | Medium | Missing clear-before-set in packed slot updates |
| T3 | Smart Contract | High | Medium | Irrevocable permissions in access control |
| T4 | Smart Contract | Medium | Low | Boolean vs bitwise OR confusion |
| T5 | Smart Contract | Low | Low | Compiler-generated patterns |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| AND (0x16) | Complementary operation; AND clears bits, OR sets them. Always AND-clear before OR-set in packed updates |
| XOR (0x18) | Toggles bits; OR only sets. XOR can be used to detect differences between two bitmaps |
| NOT (0x19) | OR(x, NOT(mask)) sets all bits except those in the mask (complement setting) |
| SHL (0x1B) | Positions values before OR-combining: (value << offset) | existingSlot |
| SHR (0x1C) | Extracts fields after OR-combined storage; complement of the SHL+OR pattern |
| SSTORE (0x55) | Packed OR values are written to storage; SSTORE gas optimization motivates packing |
| SLOAD (0x54) | Packed OR values are read from storage for extraction via AND+SHR |