Skip to main content

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:
  1. shift - number of bit positions to shift left (0-255)
  2. 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):
MethodGasNotes
SHL (Constantinople+)3Native shift
MUL (pre-EIP-145)5value * 256
EXP + MUL (variable)65+value * 2^shift
Gas savings: 2-1612 gas per shift vs pre-EIP-145 methods.

References