This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.
Overview
Bitwise operations provide low-level bit manipulation on 256-bit (32-byte) values. These operations enable efficient masking, flag management, and bit-level data packing critical for optimized smart contract implementations.
8 opcodes enable:
- Logical operations: AND, OR, XOR, NOT
- Byte extraction: BYTE
- Shift operations (EIP-145): SHL, SHR, SAR
All operations work on unsigned 256-bit integers, with shift operations introduced in the Constantinople hardfork (EIP-145).
Opcodes
| Opcode | Name | Gas | Stack In → Out | Description |
|---|
| 0x16 | AND | 3 | a, b → a&b | Bitwise AND |
| 0x17 | OR | 3 | a, b → a|b | Bitwise OR |
| 0x18 | XOR | 3 | a, b → a^b | Bitwise XOR |
| 0x19 | NOT | 3 | a → ~a | Bitwise NOT (one’s complement) |
| 0x1a | BYTE | 3 | i, x → x[i] | Extract byte at index i |
| 0x1b | SHL | 3 | shift, val → val<<shift | Shift left (Constantinople+) |
| 0x1c | SHR | 3 | shift, val → val>>shift | Logical shift right (Constantinople+) |
| 0x1d | SAR | 3 | shift, val → val>>shift | Arithmetic shift right (Constantinople+) |
Bit Manipulation Patterns
Masking
Extract specific bits using AND:
// Extract lower 160 bits (address from uint256)
const mask = (1n << 160n) - 1n; // 0x00000...000FFFFF...FFFF
const address = value & mask;
// Extract specific byte range (bytes 12-19)
const mask = 0xFFFFFFFFFFFFFFFF000000000000000000000000n << 96n;
const extracted = (value & mask) >> 96n;
Flag Management
Use individual bits as boolean flags:
// Set flags (OR)
const FLAG_A = 1n << 0n; // Bit 0
const FLAG_B = 1n << 1n; // Bit 1
const FLAG_C = 1n << 2n; // Bit 2
let flags = 0n;
flags |= FLAG_A | FLAG_C; // Enable flags A and C
// Check flags (AND)
const hasA = (flags & FLAG_A) !== 0n; // true
const hasB = (flags & FLAG_B) !== 0n; // false
// Clear flags (AND + NOT)
flags &= ~FLAG_A; // Disable flag A
// Toggle flags (XOR)
flags ^= FLAG_B; // Toggle flag B
Data Packing
Pack multiple values into single uint256:
// Pack three values: (uint64, uint96, uint96)
const packed =
(value1 << 192n) | // Upper 64 bits
(value2 << 96n) | // Middle 96 bits
value3; // Lower 96 bits
// Unpack
const value1 = packed >> 192n;
const value2 = (packed >> 96n) & ((1n << 96n) - 1n);
const value3 = packed & ((1n << 96n) - 1n);
Shift Operations (EIP-145)
EIP-145 Background
Before Constantinople (pre-EIP-145), shift operations required expensive arithmetic:
- Left shift:
value * 2^shift (MUL + EXP)
- Right shift:
value / 2^shift (DIV + EXP)
EIP-145 introduced native shift opcodes (SHL, SHR, SAR) at 3 gas each, making shifts as cheap as basic arithmetic.
Shift Direction
Stack order matters:
// SHL/SHR/SAR: shift amount is TOS (top of stack)
PUSH 8 // shift amount (TOS)
PUSH 0xFF // value
SHL // Result: 0xFF << 8 = 0xFF00
Logical vs Arithmetic Shifts
SHR (Logical Shift Right):
- Shifts bits right, filling with zeros
- Unsigned operation
- Divides by powers of 2
// 0x80...00 >> 1 = 0x40...00 (positive result)
const value = 1n << 255n; // MSB set (would be negative if signed)
const result = value >> 1n; // 0x40...00 (logical, fills with 0)
SAR (Arithmetic Shift Right):
- Shifts bits right, preserving sign bit
- Signed operation (two’s complement)
- Divides signed integers by powers of 2
// 0x80...00 >> 1 = 0xC0...00 (negative result)
const value = 1n << 255n; // MSB set (negative in two's complement)
const result_sar = sar(value, 1n); // 0xC0...00 (sign-extended)
Overflow Behavior
Shifts >= 256 bits have defined behavior:
// SHL: shift >= 256 → 0
shl(0xFF, 256n) // 0
// SHR: shift >= 256 → 0
shr(0xFF, 256n) // 0
// SAR: shift >= 256 → all bits = sign bit
sar(0xFF, 256n) // 0 (positive)
sar(1n << 255n, 256n) // 0xFFFF...FFFF (negative, all 1s)
Common Patterns
Efficient Multiplication/Division by Powers of 2
// Instead of: value * 256
assembly {
result := shl(8, value) // 3 gas vs MUL (5 gas)
}
// Instead of: value / 16
assembly {
result := shr(4, value) // 3 gas vs DIV (5 gas)
}
// Convert uint256 to address (lower 160 bits)
assembly {
addr := and(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
}
Check if Power of 2
// value & (value - 1) == 0 for powers of 2
function isPowerOfTwo(uint256 x) pure returns (bool) {
return x != 0 && (x & (x - 1)) == 0;
}
Count Set Bits (Hamming Weight)
// Brian Kernighan's algorithm
function countSetBits(uint256 x) pure returns (uint256 count) {
while (x != 0) {
x &= x - 1; // Clear lowest set bit
count++;
}
}
Bit Reversal
// Reverse bits in byte
function reverseByte(uint8 b) pure returns (uint8 result) {
result = ((b & 0xAA) >> 1) | ((b & 0x55) << 1);
result = ((result & 0xCC) >> 2) | ((result & 0x33) << 2);
result = (result >> 4) | (result << 4);
}
Zero/One Extension
// Zero-extend (logical)
const extended = value & mask;
// Sign-extend (use SIGNEXTEND opcode 0x0b)
// Or manual: check sign bit and fill
const sign = (value >> (bits - 1n)) & 1n;
const extended = sign ? value | (~0n << bits) : value;
Gas Costs
All bitwise operations cost 3 gas (GasFastestStep):
| Category | Gas | Opcodes |
|---|
| Very Low (Fastest Step) | 3 | AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR |
Comparison with arithmetic:
- Bitwise ops: 3 gas
- ADD/SUB: 3 gas
- MUL/DIV/MOD: 5 gas
- Shifts replace expensive MUL/EXP or DIV/EXP combinations (5-60+ gas → 3 gas)
Edge Cases
Maximum Values
const MAX = (1n << 256n) - 1n;
// AND: identity with all 1s
and(value, MAX) // = value
// OR: all 1s with anything
or(value, MAX) // = MAX
// XOR: NOT when XOR with all 1s
xor(value, MAX) // = ~value
// NOT: double negation
not(not(value)) // = value
// AND: zero with anything
and(0, value) // = 0
// OR: identity with zero
or(0, value) // = value
// XOR: identity with zero
xor(0, value) // = value
// NOT: all ones
not(0) // = 2^256 - 1
// BYTE: out of range index
byte(32, value) // = 0 (index >= 32)
byte(0, value) // = MSB (byte 0 is leftmost)
byte(31, value) // = LSB (byte 31 is rightmost)
Shift Edge Cases
// Zero shift
shl(0, value) // = value
shr(0, value) // = value
sar(0, value) // = value
// Shift by 256+ bits
shl(256, value) // = 0
shr(256, value) // = 0
sar(256, positive) // = 0
sar(256, negative) // = 0xFFFF...FFFF (all 1s)
Implementation
TypeScript
import * as Bitwise from '@tevm/voltaire/evm/instructions/bitwise';
// Execute bitwise operations
Bitwise.and(frame); // 0x16
Bitwise.or(frame); // 0x17
Bitwise.xor(frame); // 0x18
Bitwise.not(frame); // 0x19
Bitwise.byte(frame); // 0x1a
Bitwise.shl(frame); // 0x1b (Constantinople+)
Bitwise.shr(frame); // 0x1c (Constantinople+)
Bitwise.sar(frame); // 0x1d (Constantinople+)
Zig
const evm = @import("evm");
const BitwiseHandlers = evm.instructions.bitwise.Handlers(FrameType);
// Execute operations
try BitwiseHandlers.op_and(frame);
try BitwiseHandlers.op_or(frame);
try BitwiseHandlers.xor(frame);
try BitwiseHandlers.not(frame);
try BitwiseHandlers.byte(frame);
try BitwiseHandlers.shl(frame); // Constantinople+
try BitwiseHandlers.shr(frame); // Constantinople+
try BitwiseHandlers.sar(frame); // Constantinople+
Security Considerations
Off-by-One Errors
Bit indexing is zero-based and left-to-right (MSB to LSB):
// BYTE opcode: byte 0 is MSB (leftmost)
// Common mistake: assuming byte 0 is LSB
bytes32 data = 0x0123456789ABCDEF...;
assembly {
let b := byte(0, data) // = 0x01 (not 0xEF!)
}
Mask Construction
Incorrect masks can leak unintended bits:
// Bad: mask doesn't cover full range
uint256 mask = 0xFFFFFFFF; // Only 32 bits
uint160 addr = uint160(value & mask); // Missing upper bits!
// Good: proper mask for type
uint256 mask = type(uint160).max; // Full 160 bits
uint160 addr = uint160(value & mask);
Shift Amount Validation
// Dangerous: unchecked shift amount from user input
function shiftLeft(uint256 value, uint256 shift) returns (uint256) {
return value << shift; // shift >= 256 → 0 (may not be intended)
}
// Safer: validate bounds
require(shift < 256, "shift overflow");
Sign Extension Pitfalls
SAR treats MSB as sign bit. Ensure values are properly signed:
// Unexpected: SAR on unsigned value with MSB set
uint256 value = type(uint256).max; // All 1s
assembly {
result := sar(1, value) // = 0xFFFF...FFFF (sign-extended!)
}
// Use SHR for unsigned values
assembly {
result := shr(1, value) // = 0x7FFF...FFFF (zero-filled)
}
Benchmarks
Bitwise operations are among the fastest EVM operations:
| Operation | Gas | Relative Speed |
|---|
| AND/OR/XOR/NOT | 3 | Fastest |
| BYTE | 3 | Fastest |
| SHL/SHR/SAR | 3 | Fastest (vs 5-60+ pre-EIP-145) |
EIP-145 impact:
- Pre-Constantinople: Left shift = MUL + EXP = 5 + (10 + 50/byte) gas
- Post-Constantinople: SHL = 3 gas
- Savings: 12-1607 gas per shift operation
References