Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x16 |
| Mnemonic | AND |
| Gas | 3 |
| Stack Input | a, b |
| Stack Output | a & b |
| Behavior | Bitwise 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 << 3instead of1 << 4) - The AND result is compared with
== REQUIRED_ROLEinstead 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 & 1T3: 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 maskThese 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 indexProtocol-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 Case | Behavior | Security Implication |
|---|---|---|
x & 0 | Returns 0 | Always clears; potential logic bypass if mask is accidentally zero |
x & MAX_UINT256 | Returns x | Identity; no masking effect. May indicate a missing mask |
0 & 0 | Returns 0 | No issue |
MAX_UINT256 & MAX_UINT256 | Returns MAX_UINT256 | No masking |
| Address AND with 20-byte mask | Returns lower 160 bits | Standard address extraction; upper 96 bits cleared |
| AND with off-by-one mask | Silently drops one bit | Affects ~50% of possible values; hard to catch in testing |
| AND of two different-width masks | Returns narrower of the two | Intersection semantics may surprise developers |
x & (x - 1) | Clears lowest set bit | Used 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:
- OpenZeppelin PR #4941: Mask computed address in Create2 and Clones
- OpenZeppelin PR #5069: Fix dirty bits in Clones.sol
- OpenZeppelin PR #5195: Clean dirty addresses and booleans
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
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Dirty address bits | Always mask addresses to 160 bits in assembly | and(addr, 0xffffffffffffffffffffffffffffffffffffffff) |
| T1: Library addresses | Use OpenZeppelin >= 5.0.2 which masks computed addresses | Update dependencies; audit inline assembly |
| T2: Permission bitmap bugs | Use parentheses around AND comparisons; use named functions | require((roles & MASK) != 0) with explicit parens |
| T2: Role confusion | Use OpenZeppelin AccessControl instead of raw bitmaps | Battle-tested role management with event logging |
| T3: Packed field extraction | Define mask constants once; unit test boundary values | uint256 constant BALANCE_MASK = (1 << 96) - 1; |
| T4: Address truncation | Use Solidity type casting instead of raw AND | address(uint160(value)) instead of value & ADDR_MASK |
| T5: Token ID masks | Fuzz-test packed ID boundaries; define masks from type widths | Test 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 ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Medium | OpenZeppelin Clones/Create2 (2024), Address verification ($11.2B TVL) |
| T2 | Smart Contract | High | Medium | Operator precedence bugs in access control |
| T3 | Smart Contract | High | Medium | Packed storage extraction in DeFi vaults |
| T4 | Smart Contract | Medium | Low | Assembly-level address handling |
| T5 | Smart Contract | Medium | Low | ERC-1155 token ID packing |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
| P3 | Protocol | Low | Low | CVE-2024-45056 (zksolc bitmask width) |
Related Opcodes
| Opcode | Relationship |
|---|---|
| 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 |