Opcode Summary

PropertyValue
Opcode0x16
MnemonicAND
Gas3
Stack Inputa, b
Stack Outputa & b
BehaviorBitwise AND of two 256-bit values. Each bit of the result is 1 only if the corresponding bits of both operands are 1.

Threat Surface

AND is the EVM’s primary masking and filtering opcode. Its security significance comes not from what it does, but from what developers expect it to do — and how often those expectations are subtly wrong. AND is used pervasively for:

  • Address extraction: Masking keccak256 hashes to 160-bit addresses (hash & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
  • Permission and role bitmaps: Checking permissions & ROLE_ADMIN != 0
  • Packed storage unpacking: Extracting fields from tightly packed uint256 slots
  • Type truncation: Narrowing uint256 to uint160, uint128, uint96, etc.

The danger lies in mask construction errors — an off-by-one bit in a mask, a mask that doesn’t cover the full width of the target type, or a mask applied in the wrong byte position. Unlike arithmetic overflow which produces dramatically wrong results, bitmask errors often produce results that look correct for common inputs but fail on edge cases, making them exceptionally difficult to catch in testing.

The EVM’s big-endian word format (most significant byte at the lowest address) adds another layer of confusion. Developers accustomed to little-endian systems may construct masks with the bytes in the wrong order, particularly when extracting addresses or bytes from hashes.


Smart Contract Threats

T1: Dirty Higher-Order Bits in Address Masking (Critical)

The EVM stores all values in 256-bit words, but Ethereum addresses are only 160 bits. When addresses are loaded from calldata, computed via CREATE2/keccak256, or passed through inline assembly, the upper 96 bits may contain non-zero “dirty” data. If these dirty bits are not masked off before comparison or use, critical identity checks can fail or collide.

Why it matters: OpenZeppelin discovered this exact issue in their Clones.sol and Create2 libraries in 2024. The computeAddress() function returned keccak256 results without masking the upper 96 bits. When these “dirty” addresses were passed to downstream assembly, calls failed or resolved to wrong targets.

The Solidity compiler normally cleans dirty bits before operations on typed variables, but this guarantee does not extend to inline assembly or cross-function Yul code. Any contract that computes addresses in assembly and doesn’t apply AND(addr, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) is vulnerable.

T2: Incorrect Permission Bitmask Checks (High)

Role-based access control systems commonly use bitmaps where each bit represents a permission. The pattern require(userRoles & REQUIRED_ROLE != 0) is standard. Errors occur when:

  • The mask constant has the wrong bit set (e.g., 1 << 3 instead of 1 << 4)
  • The AND result is compared with == REQUIRED_ROLE instead of != 0, which fails when the user has additional roles set
  • Multiple roles are checked with a single AND but the logic should be OR (any role) vs AND (all roles)
// Bug: requires EXACT match, not membership
require(userRoles & ADMIN_ROLE == ADMIN_ROLE);  // Correct: all bits in ADMIN_ROLE must be set
require(userRoles & ADMIN_ROLE != 0);           // Different: any bit overlap suffices
 
// Bug: operator precedence -- AND binds tighter than != in some contexts
// In Solidity, & has lower precedence than ==, so:
// userRoles & ADMIN_ROLE == ADMIN_ROLE
// is parsed as: userRoles & (ADMIN_ROLE == ADMIN_ROLE) -> userRoles & 1

T3: Packed Storage Extraction Errors (High)

Contracts that pack multiple values into a single uint256 storage slot use AND with position-shifted masks to extract individual fields. Errors in mask width, shift amount, or mask position silently extract wrong data:

// Extracting a uint96 from bits 160-255 of a packed slot
uint96 value = uint96((packed >> 160) & 0xFFFFFFFFFFFFFFFFFFFFFFFF);  // 96-bit mask
 
// Bug: mask is 88 bits, not 96 -- silently truncates values > 2^88
uint96 buggy = uint96((packed >> 160) & 0xFFFFFFFFFFFFFFFFFFFFFF);    // 88-bit mask

These errors are insidious because they only manifest when the extracted value exceeds the mask width, which may not occur in normal testing but can be triggered by adversarial inputs.

T4: Address Truncation via AND Instead of Type Casting (Medium)

Some contracts use AND(value, 2^160 - 1) to extract addresses from 256-bit values instead of proper Solidity type casting (address(uint160(value))). While functionally equivalent for clean inputs, the AND approach skips Solidity’s built-in dirty-bit cleaning and can introduce subtle issues in complex assembly sequences.

T5: Bitmask Width Errors in Token Standards (Medium)

ERC-1155 and other multi-token standards pack token IDs and amounts into uint256 values using bitmasks. A mask that’s one bit too narrow or too wide can cause token ID collisions or amount truncation:

// ERC-1155 style: upper 128 bits = token type, lower 128 bits = token index
uint256 TOKEN_TYPE_MASK = uint256(type(uint128).max) << 128;
uint256 tokenType = id & TOKEN_TYPE_MASK;
 
// If mask is accidentally (type(uint128).max) << 127, one bit of the type leaks into the index

Protocol-Level Threats

P1: No DoS Vector (Low)

AND 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)

AND is trivially deterministic: each bit of the result depends only on the corresponding bits of the two operands. All EVM client implementations agree on its behavior. No consensus divergence has occurred due to AND.

P3: Compiler Optimization of AND (Low)

The Solidity compiler uses AND extensively for type narrowing and dirty-bit cleaning. Different compiler versions may emit different AND mask patterns for the same Solidity source. CVE-2024-45056 demonstrated that compiler-level optimization of bitwise operations (specifically XOR+SHL folding in zksolc) can produce incorrect bit widths, where ~1 was generated as 64-bit instead of 256-bit. While this specific bug was in XOR/SHL, the same class of error applies to any compiler optimization involving AND masks.


Edge Cases

Edge CaseBehaviorSecurity Implication
x & 0Returns 0Always clears; potential logic bypass if mask is accidentally zero
x & MAX_UINT256Returns xIdentity; no masking effect. May indicate a missing mask
0 & 0Returns 0No issue
MAX_UINT256 & MAX_UINT256Returns MAX_UINT256No masking
Address AND with 20-byte maskReturns lower 160 bitsStandard address extraction; upper 96 bits cleared
AND with off-by-one maskSilently drops one bitAffects ~50% of possible values; hard to catch in testing
AND of two different-width masksReturns narrower of the twoIntersection semantics may surprise developers
x & (x - 1)Clears lowest set bitUsed in popcount patterns; safe but non-obvious

Real-World Exploits

Exploit 1: OpenZeppelin Clones/Create2 Dirty Bits — Library-Wide Fix (2024)

Root cause: Address values computed from keccak256 were not masked via AND, leaving dirty upper bits that caused downstream assembly failures.

Details: OpenZeppelin’s Create2.computeAddress() and Clones.predictDeterministicAddress() returned 256-bit keccak256 results without masking the upper 96 bits to produce clean 160-bit addresses. When these values were passed to inline assembly code that assumed clean address inputs, calls could fail or resolve to incorrect targets. The fix applied AND(result, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) to mask computed addresses.

The issue was addressed across three PRs in 2024: PR #4941 (March — initial masking), PR #5069 (June — fix for regression), and PR #5195 (September — comprehensive cleanup of dirty addresses and booleans across the library).

AND’s role: The fix was precisely the addition of AND masking. The vulnerability existed because AND was not applied where it should have been.

References:


Exploit 2: Address Verification Vulnerabilities — $11.2B TVL at Risk (2024)

Root cause: Improper address extraction and verification in smart contracts, where bitmask-based address extraction from calldata was performed incorrectly.

Details: A USENIX Security 2024 paper (“All Your Tokens are Belong to Us”) systematically analyzed address verification vulnerabilities across Ethereum smart contracts. The researchers discovered 812 previously undisclosed vulnerable contracts with over $11.2 billion in total value locked. A key vulnerability class involved improper extraction of addresses from packed calldata — particularly in meta-transaction contexts (ERC-2771) where the sender address is appended to calldata and extracted via bitmask operations.

In the December 2023 Time token attack ($190K stolen), the attacker crafted malicious calldata that caused the contract to extract the wrong 20 bytes as the caller address. The address extraction relied on AND masking of calldata, and the attacker manipulated the data layout to shift which bytes the mask captured.

AND’s role: Address extraction from packed data fundamentally relies on AND masking. When the mask is applied to the wrong position or the data is laid out differently than expected, the extracted address is attacker-controlled.

References:


Exploit 3: CVE-2024-45056 — zksolc Bitwise Operation Misoptimization (August 2024)

Root cause: The zksolc compiler (ZKsync’s Solidity compiler) incorrectly optimized a XOR(SHL(1, x), -1) expression, generating ~1 as a 64-bit value instead of 256-bit, producing incorrect bitmask widths.

Details: When LLVM optimizations folded the expression (xor (shl 1, x), -1) to (rotl ~1, x), the complement ~1 was generated as 2^64 - 1 instead of 2^256 - 1. This meant the resulting bitmask was 192 bits too narrow, potentially affecting any contract that used this pattern for mask construction. The vulnerability affected all zksolc versions prior to 1.5.3.

AND’s role: While the bug was in XOR/SHL optimization, its effect was to produce an incorrectly-sized bitmask — the same class of error as a wrong AND mask. The resulting value, when used as a mask, would fail to cover the full 256-bit word.

References:


Attack Scenarios

Scenario A: Dirty Address Bits Bypass Access Control

// Inline assembly computes an address but doesn't mask it
function computeTarget(bytes32 salt) internal view returns (address) {
    address target;
    assembly {
        let hash := keccak256(0x00, 0x55)  // CREATE2 hash
        target := hash  // Upper 96 bits are dirty!
    }
    return target;  // Dirty address returned
}
 
// Downstream comparison fails
function isAuthorized(address user) external view returns (bool) {
    address expected = computeTarget(salt);
    // If expected has dirty bits: expected != cleanAddress even if lower 160 bits match
    return user == expected;  // May always return false
}

Scenario B: Permission Bitmap Operator Precedence Bug

contract VulnerableACL {
    uint256 constant ROLE_ADMIN = 1 << 0;   // 0x01
    uint256 constant ROLE_MINTER = 1 << 1;  // 0x02
    mapping(address => uint256) public roles;
    
    function mint(address to, uint256 amount) external {
        // Bug: Solidity evaluates == before &
        // This parses as: roles[msg.sender] & (ROLE_MINTER == ROLE_MINTER)
        // Which is: roles[msg.sender] & 1
        // Which checks ROLE_ADMIN, not ROLE_MINTER!
        require(roles[msg.sender] & ROLE_MINTER == ROLE_MINTER);
        _mint(to, amount);
    }
}

Attack: A user with only ROLE_ADMIN (bit 0) passes the check intended for ROLE_MINTER (bit 1).

Scenario C: Packed Storage Field Extraction Error

contract PackedVault {
    // Packed: [96 bits: balance][160 bits: owner]
    mapping(uint256 => uint256) internal _vaultData;
    
    function getBalance(uint256 vaultId) public view returns (uint96) {
        uint256 packed = _vaultData[vaultId];
        // Bug: mask is 80 bits, not 96 -- drops top 16 bits of balance
        return uint96((packed >> 160) & 0xFFFFFFFFFFFFFFFFFFFF);
        // Should be: & 0xFFFFFFFFFFFFFFFFFFFFFFFF (24 hex chars = 96 bits)
    }
}

Attack: Deposits > 2^80 wei (~1.2M ETH) have their balance silently truncated. Withdrawing reports less than deposited.

Scenario D: ERC-2771 Address Spoofing via Mask Position

// Trusted forwarder appends msg.sender (20 bytes) to calldata
// Contract extracts it via AND mask on the last 20 bytes
function _msgSender() internal view returns (address sender) {
    if (msg.sender == trustedForwarder) {
        assembly {
            sender := shr(96, calldataload(sub(calldatasize(), 20)))
        }
    } else {
        sender = msg.sender;
    }
    // If attacker nests a Multicall inside a meta-tx, calldatasize()
    // points to the wrong position, extracting attacker-controlled bytes
}

Mitigations

ThreatMitigationImplementation
T1: Dirty address bitsAlways mask addresses to 160 bits in assemblyand(addr, 0xffffffffffffffffffffffffffffffffffffffff)
T1: Library addressesUse OpenZeppelin >= 5.0.2 which masks computed addressesUpdate dependencies; audit inline assembly
T2: Permission bitmap bugsUse parentheses around AND comparisons; use named functionsrequire((roles & MASK) != 0) with explicit parens
T2: Role confusionUse OpenZeppelin AccessControl instead of raw bitmapsBattle-tested role management with event logging
T3: Packed field extractionDefine mask constants once; unit test boundary valuesuint256 constant BALANCE_MASK = (1 << 96) - 1;
T4: Address truncationUse Solidity type casting instead of raw ANDaddress(uint160(value)) instead of value & ADDR_MASK
T5: Token ID masksFuzz-test packed ID boundaries; define masks from type widthsTest with values at 2^n-1 and 2^n for each field boundary

Compiler/EIP-Based Protections

  • Solidity >= 0.8.0: The compiler automatically cleans dirty bits when storing to memory or performing comparisons on typed variables. This protection does not apply inside assembly { } blocks.
  • Static analysis: Slither detects some unsafe type casts and missing address validations. Custom detectors can flag AND masks that don’t match their target type width.
  • Formal verification: Tools like Certora can verify that bitmask operations preserve type invariants across all possible inputs.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalMediumOpenZeppelin Clones/Create2 (2024), Address verification ($11.2B TVL)
T2Smart ContractHighMediumOperator precedence bugs in access control
T3Smart ContractHighMediumPacked storage extraction in DeFi vaults
T4Smart ContractMediumLowAssembly-level address handling
T5Smart ContractMediumLowERC-1155 token ID packing
P1ProtocolLowN/A
P2ProtocolLowN/A
P3ProtocolLowLowCVE-2024-45056 (zksolc bitmask width)

OpcodeRelationship
OR (0x17)Complementary operation; OR sets bits where AND clears them. Combined with AND for read-modify-write patterns on packed storage
XOR (0x18)Toggles bits; AND masks bits. XOR can be used to compute the difference between two bitmaps
NOT (0x19)Inverts all bits; AND(x, NOT(mask)) clears the masked bits (complement masking)
BYTE (0x1A)Extracts a single byte; AND with 0xFF after shifting is the equivalent for arbitrary positions
SHL (0x1B)Used to position masks before AND extraction: AND(SHR(n, x), mask)
SHR (0x1C)Used to align extracted fields to bit 0 after AND masking
CALLDATALOAD (0x35)Loads 32-byte words from calldata; AND is used to extract sub-word fields from loaded data