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
Opcode: 0x1b
Introduced: Constantinople (EIP-145)
SHL performs logical shift left on a 256-bit value, shifting bits toward the most significant position. Vacated bits (on the right) are filled with zeros. This operation efficiently multiplies by powers of 2 and is critical for bit manipulation and data packing.
Before EIP-145, left shifts required expensive MUL + EXP operations (5-60+ gas). SHL reduces this to 3 gas.
Specification
Stack Input:
shift (top) - number of bit positions to shift
value - 256-bit value to shift
Stack Output:
value << shift (mod 2^256)
Gas Cost: 3 (GasFastestStep)
Hardfork: Constantinople (EIP-145) or later
Shift Behavior:
shift < 256: value << shift (wraps at 256 bits)
shift >= 256: 0 (all bits shifted out)
Behavior
SHL pops two values from the stack:
- shift - number of bit positions to shift left (0-255)
- value - 256-bit value to be shifted
Result is value shifted left by shift positions, with zeros filling vacated bits. If shift >= 256, result is 0 (all bits shifted out).
Overflow: High-order bits are discarded (wraps at 256 bits).
Examples
Basic Left Shift
import { shl } from '@tevm/voltaire/evm/bitwise';
import { createFrame } from '@tevm/voltaire/evm/Frame';
// Shift 0xFF left by 8 bits (multiply by 256)
const frame = createFrame({ stack: [8n, 0xFFn] });
const err = shl(frame);
console.log(frame.stack[0].toString(16)); // 'ff00'
Multiply by Power of 2
// Shift left by N = multiply by 2^N
// 5 << 3 = 5 * 8 = 40
const frame = createFrame({ stack: [3n, 5n] });
shl(frame);
console.log(frame.stack[0]); // 40n
Zero Shift (Identity)
// Shift by 0 positions = identity
const value = 0x123456n;
const frame = createFrame({ stack: [0n, value] });
shl(frame);
console.log(frame.stack[0] === value); // true
Maximum Shift (Overflow)
// Shift >= 256 results in 0
const value = 0xFFFFFFFFn;
const frame = createFrame({ stack: [256n, value] });
shl(frame);
console.log(frame.stack[0]); // 0n
Partial Overflow
// High bits are discarded
const value = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFn;
const frame = createFrame({ stack: [4n, value] });
shl(frame);
// Result: lower 252 bits are all 1s, upper 4 bits are 0
const expected = ((1n << 256n) - 1n) - ((1n << 4n) - 1n);
console.log(frame.stack[0] === expected); // true
Pack Address into uint256
// Shift address to upper bits (leave lower bits for flags)
const address = 0xdEaDbEeFcAfE1234567890ABCDEf1234567890ABn;
const frame = createFrame({ stack: [96n, address] }); // Shift left 96 bits
shl(frame);
// Address now in upper 160 bits, lower 96 bits available for data
console.log(frame.stack[0].toString(16));
Gas Cost
Cost: 3 gas (GasFastestStep)
Pre-EIP-145 equivalent:
// Before Constantinople: expensive!
result = value * (2 ** shift) // MUL (5 gas) + EXP (10 + 50/byte gas)
// Total: 15-1615 gas
// After Constantinople: cheap!
assembly { result := shl(shift, value) } // 3 gas
Savings: 12-1612 gas per shift operation.
Edge Cases
Zero Value
// Shifting zero always yields zero
const frame = createFrame({ stack: [100n, 0n] });
shl(frame);
console.log(frame.stack[0]); // 0n
Zero Shift
// No shift = identity
const value = 0xDEADBEEFn;
const frame = createFrame({ stack: [0n, value] });
shl(frame);
console.log(frame.stack[0] === value); // true
Shift by 1 (Double)
// Shift left by 1 = multiply by 2
const value = 42n;
const frame = createFrame({ stack: [1n, value] });
shl(frame);
console.log(frame.stack[0]); // 84n
Shift by 255 (Near Max)
// Shift to MSB position
const value = 1n;
const frame = createFrame({ stack: [255n, value] });
shl(frame);
const expected = 1n << 255n; // 0x8000...0000
console.log(frame.stack[0] === expected); // true
Shift by 256+ (Complete Overflow)
// Any shift >= 256 yields 0
for (const shift of [256n, 257n, 1000n, (1n << 200n)]) {
const frame = createFrame({ stack: [shift, 0xFFFFn] });
shl(frame);
console.log(frame.stack[0]); // 0n for all
}
Large Value, Small Shift
// Shifting MAX - some bits overflow
const MAX = (1n << 256n) - 1n;
const frame = createFrame({ stack: [1n, MAX] });
shl(frame);
// Result: all bits set except LSB
const expected = MAX - 1n;
console.log(frame.stack[0] === expected); // true
Hardfork Check (Pre-Constantinople)
// SHL is invalid before Constantinople
const frame = createFrame({
stack: [8n, 0xFFn],
hardfork: 'byzantium' // Before Constantinople
});
const err = shl(frame);
console.log(err); // { type: "InvalidOpcode" }
Stack Underflow
// Insufficient stack items
const frame = createFrame({ stack: [8n] });
const err = shl(frame);
console.log(err); // { type: "StackUnderflow" }
Out of Gas
// Insufficient gas
const frame = createFrame({ stack: [8n, 0xFFn], gasRemaining: 2n });
const err = shl(frame);
console.log(err); // { type: "OutOfGas" }
console.log(frame.gasRemaining); // 0n
Common Usage
Multiply by Power of 2
// Efficient multiplication by 256
assembly {
result := shl(8, value) // 3 gas vs MUL (5 gas)
}
Pack Data Fields
// Pack timestamp (40 bits) + amount (216 bits)
function pack(uint40 timestamp, uint216 amount) pure returns (uint256) {
return (uint256(timestamp) << 216) | uint256(amount);
// Or in assembly:
// assembly {
// result := or(shl(216, timestamp), amount)
// }
}
Align to Byte Boundary
// Shift to align data to byte boundary
function alignToBytes(uint256 value, uint256 bytePos) pure returns (uint256) {
assembly {
result := shl(mul(8, bytePos), value)
}
}
Create Bit Mask
// Create mask with N consecutive ones at position P
function createMask(uint256 numBits, uint256 position) pure returns (uint256) {
require(numBits + position <= 256, "overflow");
uint256 mask = (1 << numBits) - 1;
return mask << position;
// assembly { result := shl(position, sub(shl(numBits, 1), 1)) }
}
Scale Fixed-Point Numbers
// Fixed-point arithmetic: shift to scale by 10^18
uint256 constant SCALE = 1e18;
function toFixedPoint(uint256 value) pure returns (uint256) {
// In practice, use MUL for non-power-of-2 scaling
// But for powers of 2 (e.g., binary fixed-point):
return value << 64; // Q64.64 fixed-point
}
Efficient Array Indexing
// Calculate array slot offset (element size * index)
// For 32-byte elements: index << 5 (multiply by 32)
function getArraySlot(uint256 baseSlot, uint256 index) pure returns (uint256) {
assembly {
let offset := shl(5, index) // index * 32
mstore(0, add(baseSlot, offset))
return(0, 32)
}
}
Implementation
/**
* SHL opcode (0x1b) - Shift left operation (EIP-145)
*/
export function shl(frame: FrameType): EvmError | null {
// Check hardfork (Constantinople or later)
if (frame.hardfork.isBefore('constantinople')) {
return { type: "InvalidOpcode" };
}
// Consume gas (GasFastestStep = 3)
frame.gasRemaining -= 3n;
if (frame.gasRemaining < 0n) {
frame.gasRemaining = 0n;
return { type: "OutOfGas" };
}
// Pop operands
if (frame.stack.length < 2) return { type: "StackUnderflow" };
const shift = frame.stack.pop();
const value = frame.stack.pop();
// Compute shift left (with overflow handling)
const result = shift >= 256n
? 0n
: (value << shift) & ((1n << 256n) - 1n);
// Push result
if (frame.stack.length >= 1024) return { type: "StackOverflow" };
frame.stack.push(result);
// Increment PC
frame.pc += 1;
return null;
}
Testing
Test Coverage
import { describe, it, expect } from 'vitest';
import { shl } from './shl.js';
describe('SHL (0x1b)', () => {
it('shifts left by 8 bits', () => {
const frame = createFrame({ stack: [8n, 0xFFn] });
expect(shl(frame)).toBeNull();
expect(frame.stack[0]).toBe(0xFF00n);
});
it('multiplies by power of 2', () => {
const frame = createFrame({ stack: [3n, 5n] });
expect(shl(frame)).toBeNull();
expect(frame.stack[0]).toBe(40n); // 5 * 2^3
});
it('handles zero shift (identity)', () => {
const value = 0x123456n;
const frame = createFrame({ stack: [0n, value] });
expect(shl(frame)).toBeNull();
expect(frame.stack[0]).toBe(value);
});
it('returns 0 for shift >= 256', () => {
const frame = createFrame({ stack: [256n, 0xFFFFFFFFn] });
expect(shl(frame)).toBeNull();
expect(frame.stack[0]).toBe(0n);
});
it('handles partial overflow', () => {
const value = 0xFFn;
const frame = createFrame({ stack: [252n, value] });
expect(shl(frame)).toBeNull();
// 0xFF << 252 = 0xFF00...0000 (252 zeros)
const expected = 0xFFn << 252n;
expect(frame.stack[0]).toBe(expected);
});
it('shifts MAX value by 1', () => {
const MAX = (1n << 256n) - 1n;
const frame = createFrame({ stack: [1n, MAX] });
expect(shl(frame)).toBeNull();
// All bits except LSB
expect(frame.stack[0]).toBe(MAX - 1n);
});
it('shifts 1 to MSB position', () => {
const frame = createFrame({ stack: [255n, 1n] });
expect(shl(frame)).toBeNull();
expect(frame.stack[0]).toBe(1n << 255n);
});
it('returns InvalidOpcode before Constantinople', () => {
const frame = createFrame({
stack: [8n, 0xFFn],
hardfork: 'byzantium'
});
expect(shl(frame)).toEqual({ type: 'InvalidOpcode' });
});
it('returns StackUnderflow with insufficient stack', () => {
const frame = createFrame({ stack: [8n] });
expect(shl(frame)).toEqual({ type: 'StackUnderflow' });
});
it('returns OutOfGas when insufficient gas', () => {
const frame = createFrame({ stack: [8n, 0xFFn], gasRemaining: 2n });
expect(shl(frame)).toEqual({ type: 'OutOfGas' });
});
});
Edge Cases Tested
- Basic shift operations
- Multiplication by powers of 2
- Zero shift (identity)
- Shift >= 256 (overflow to zero)
- Partial overflow
- Maximum value shifts
- Shift to MSB position
- Zero value shifts
- Hardfork compatibility
- Stack underflow
- Out of gas
Security
Unchecked Shift Amount
// RISKY: User-controlled shift without bounds
function shiftLeft(uint256 value, uint256 shift) pure returns (uint256) {
return value << shift; // shift >= 256 → 0 (may not be intended)
}
// SAFER: Validate shift range
function shiftLeft(uint256 value, uint256 shift) pure returns (uint256) {
require(shift < 256, "shift overflow");
return value << shift;
}
Overflow Assumptions
// WRONG: Assuming shifted value is always larger
function packData(uint96 flags, uint160 addr) pure returns (uint256) {
uint256 packed = (uint256(flags) << 160) | uint256(addr);
require(packed > addr, "packing failed"); // FALSE if flags = 0!
return packed;
}
// CORRECT: Proper validation
function packData(uint96 flags, uint160 addr) pure returns (uint256) {
return (uint256(flags) << 160) | uint256(addr);
// No assumption about relative magnitude
}
Data Loss from Overflow
// DANGEROUS: High bits silently discarded
function encode(uint128 high, uint128 low) pure returns (uint256) {
// If high > type(uint128).max, upper bits are lost!
return (uint256(high) << 128) | uint256(low);
}
// SAFER: Validate inputs
function encode(uint128 high, uint128 low) pure returns (uint256) {
// Type system enforces high <= type(uint128).max
return (uint256(high) << 128) | uint256(low);
}
Incorrect Multiplication
// Use SHL only for powers of 2
function multiply(uint256 a, uint256 b) pure returns (uint256) {
// WRONG: Only works if b is a power of 2
return a << b; // Treats b as exponent, not multiplicand!
}
// CORRECT: Use MUL for general multiplication
function multiply(uint256 a, uint256 b) pure returns (uint256) {
return a * b;
}
Benchmarks
SHL is one of the fastest EVM operations:
Execution time (relative):
- SHL: 1.0x (baseline, fastest tier)
- SHR/SAR: 1.0x (same tier)
- MUL: 1.2x
- DIV: 2.5x
Gas comparison (left shift by 8):
| Method | Gas | Notes |
|---|
| SHL (Constantinople+) | 3 | Native shift |
| MUL (pre-EIP-145) | 5 | value * 256 |
| EXP + MUL (variable) | 65+ | value * 2^shift |
Gas savings: 2-1612 gas per shift vs pre-EIP-145 methods.
References