Opcode Summary

PropertyValue
Opcode0x17
MnemonicOR
Gas3
Stack Inputa, b
Stack Outputa | b
BehaviorBitwise 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 persist

This 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 CaseBehaviorSecurity Implication
x | 0Returns xIdentity; OR with zero has no effect
x | MAX_UINT256Returns MAX_UINT256All bits set regardless of x; potential saturation bug
x | xReturns xIdempotent; no issue
x | ~xReturns MAX_UINT256Value and complement cover all bits
OR of overlapping fieldsBits from both fields mergeSilent data corruption in packed slots
OR of identical field positionsHigher value dominatesCannot distinguish which field contributed which bits
0 | 0Returns 0No issue
Large value OR small valueSmall value’s bits absorbedMay 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

ThreatMitigationImplementation
T1: Field overlapDefine field boundaries as constants; verify they don’t overlapassert(FIELD_A_END <= FIELD_B_START) in tests
T1: ClobberingUse struct packing via Solidity compiler instead of manual ORLet struct { uint96 balance; address owner; } handle layout
T2: Missing clearAlways AND-clear before OR-set for packed updatesslot = (slot & ~MASK) | (newValue << OFFSET)
T2: One-directional updatesFuzz-test with decreasing values, not just increasingTest transitions: large→small, all-ones→all-zeros
T3: Missing revocationImplement AND-NOT revocation for every OR-grantpermissions &= ~role for revocation
T4: Boolean confusionUse bool type and || for boolean logic; uint and | for bitwiseLinter rules to flag | on boolean-like variables

Best Practices for Packed Storage

  • Use libraries: OpenZeppelin’s BitMaps and SlotDerivation provide 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 IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalMediumPacked storage bugs in DeFi protocols
T2Smart ContractHighMediumMissing clear-before-set in packed slot updates
T3Smart ContractHighMediumIrrevocable permissions in access control
T4Smart ContractMediumLowBoolean vs bitwise OR confusion
T5Smart ContractLowLowCompiler-generated patterns
P1ProtocolLowN/A
P2ProtocolLowN/A

OpcodeRelationship
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