Skip to main content
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: 0x5e Introduced: Cancun (EIP-5656) Deprecated: Never MCOPY copies a region of memory from source to destination. It handles overlapping regions correctly using an internal temporary buffer. This is the first memory-to-memory copy opcode in the EVM, replacing manual byte-by-byte loops with a single atomic operation. Before Cancun, copying memory required loops with MLOAD/MSTORE or MSTORE8, which was inefficient and error-prone for overlapping regions.

Specification

Stack Input:
dest   (top)     - Destination address
src             - Source address
len             - Number of bytes to copy
Stack Output:
(empty)
Gas Cost: 3 + memory expansion cost + copy cost Copy cost formula:
copy_cost = ceil(len / 32) * 3  (3 gas per word)
Operation:
for i in range(len):
    memory[dest + i] = memory[src + i]

Behavior

MCOPY pops three values from stack: dest (top), src (middle), len (bottom). It copies len bytes from src to dest, expanding memory as needed.
  • All addresses interpreted as unsigned 256-bit integers
  • Copy handles overlapping regions correctly (atomic, not in-place)
  • Memory expansion covers both source AND destination ranges
  • Zero-length copy (len=0) charges only base gas, no expansion
  • Expansion cost quadratic; copy cost linear in words
Stack order note: Different from most opcodes - destination popped first.

Examples

Basic Copy

import { mcopy } from '@tevm/voltaire/evm/instructions/memory';
import { createFrame } from '@tevm/voltaire/evm/Frame';

const frame = createFrame();

// Write source data (bytes 0-31)
for (let i = 0; i < 32; i++) {
  frame.memory.set(i, i + 1);
}

frame.stack.push(32n);   // len
frame.stack.push(0n);    // src
frame.stack.push(64n);   // dest

const err = mcopy(frame);

// Check destination has copied data
for (let i = 0; i < 32; i++) {
  console.log(frame.memory.get(64 + i));  // i + 1
}

console.log(frame.pc);  // 1 (incremented)

Zero-Length Copy

const frame = createFrame();

frame.stack.push(0n);   // len (zero)
frame.stack.push(0n);   // src
frame.stack.push(0n);   // dest

mcopy(frame);

// No memory expansion
console.log(frame.memorySize);  // 0
console.log(frame.gasRemaining);  // Original - 3 (only base gas)

Forward Overlap (Non-Destructive)

const frame = createFrame();

// Source data at offset 0-63
for (let i = 0; i < 64; i++) {
  frame.memory.set(i, i);
}

// Copy 32 bytes from offset 0 to offset 16
// Result: bytes 0-15 stay same, bytes 16-31 duplicated, bytes 32-63 stay same
frame.stack.push(32n);   // len
frame.stack.push(0n);    // src
frame.stack.push(16n);   // dest

mcopy(frame);

// Verify: bytes 16-31 now contain copy of bytes 0-15
for (let i = 0; i < 32; i++) {
  console.log(frame.memory.get(16 + i));  // i (copied from src)
}

Backward Overlap (Non-Destructive)

const frame = createFrame();

// Source data at offset 16-47
for (let i = 0; i < 64; i++) {
  frame.memory.set(16 + i, 100 + i);
}

// Copy 32 bytes from offset 16 to offset 0
// Uses temporary buffer - no in-place issues
frame.stack.push(32n);   // len
frame.stack.push(16n);   // src
frame.stack.push(0n);    // dest

mcopy(frame);

// Bytes 0-31 now contain what was at 16-47
for (let i = 0; i < 32; i++) {
  console.log(frame.memory.get(i));  // 100 + i
}

Exact Overlap (Same Source and Destination)

const frame = createFrame();

// Write pattern
for (let i = 0; i < 32; i++) {
  frame.memory.set(i, i + 50);
}

// Copy to itself
frame.stack.push(32n);   // len
frame.stack.push(0n);    // src
frame.stack.push(0n);    // dest

mcopy(frame);

// Data unchanged
for (let i = 0; i < 32; i++) {
  console.log(frame.memory.get(i));  // i + 50
}

Large Copy

const frame = createFrame();

// Write 256 bytes
for (let i = 0; i < 256; i++) {
  frame.memory.set(i, i & 0xFF);
}

// Copy 256 bytes from offset 0 to offset 1000
frame.stack.push(256n);     // len
frame.stack.push(0n);       // src
frame.stack.push(1000n);    // dest

mcopy(frame);

// Verify copy
for (let i = 0; i < 256; i++) {
  console.log(frame.memory.get(1000 + i));  // i & 0xFF
}

// Memory expanded to cover both ranges
console.log(frame.memorySize);  // >= 1256 (word-aligned)

Gas Cost

Base cost: 3 gas Memory expansion: Quadratic, covers max(src+len, dest+len) Copy cost: 3 gas per word (rounded up) Formula:
words_required_src = ceil((src + len) / 32)
words_required_dest = ceil((dest + len) / 32)
max_words = max(words_required_src, words_required_dest)

expansion_cost = (max_words)² / 512 + 3 * (max_words - words_old)
copy_words = ceil(len / 32)
copy_cost = copy_words * 3

total_cost = 3 + expansion_cost + copy_cost
Examples:
  • Copy 32 bytes (1 word), no expansion: 3 + 0 + 3 = 6 gas
  • Copy 64 bytes (2 words), no expansion: 3 + 0 + 6 = 9 gas
  • Copy 33 bytes (2 words, rounded up): 3 + 0 + 6 = 9 gas
  • Copy with large expansion: 3 + exp(max_range) + copy_cost
Zero-length copy charges only 3 gas (no expansion, no copy cost).

Hardfork Availability

MCOPY is only available on Cancun and later:
// Error before Cancun
frame.evm.hardfork = 'Shanghai';
const err = mcopy(frame);
console.log(err);  // { type: "InvalidOpcode" }

// Works on Cancun+
frame.evm.hardfork = 'Cancun';
const err = mcopy(frame);
console.log(err);  // null (no error)
Attempting MCOPY on pre-Cancun chains reverts with InvalidOpcode.

Edge Cases

Uninitialized Source

const frame = createFrame();

// Copy from uninitialized memory (all zeros)
frame.stack.push(32n);   // len
frame.stack.push(0n);    // src (uninitialized)
frame.stack.push(64n);   // dest

mcopy(frame);

// Destination has zeros
for (let i = 0; i < 32; i++) {
  console.log(frame.memory.get(64 + i));  // 0
}

Massive Offset

const frame = createFrame({ gasRemaining: 100000000n });

// Copy from very high offset
frame.stack.push(32n);      // len
frame.stack.push(10000000n); // src (huge offset)
frame.stack.push(10000064n); // dest

mcopy(frame);

// Memory expands to accommodate (very expensive)
console.log(frame.gasRemaining);  // Significantly reduced

Out of Gas During Expansion

const frame = createFrame({ gasRemaining: 100n });

// Insufficient gas for memory expansion
frame.stack.push(10000n);   // len (large)
frame.stack.push(0n);       // src
frame.stack.push(10000n);   // dest

const err = mcopy(frame);
console.log(err);  // { type: "OutOfGas" }

Stack Underflow

const frame = createFrame();

// Only two values on stack
frame.stack.push(0n);
frame.stack.push(0n);

const err = mcopy(frame);
console.log(err);  // { type: "StackUnderflow" }

Common Usage

Copy Constructor Arguments

// Copy calldata to memory for processing
assembly {
    let calldata_size := calldatasize()
    mcopy(0x20, 0, calldata_size)  // Copy calldata to memory at 0x20
}

Cache Optimization

assembly {
    // Copy frequently accessed data to free memory for faster access
    let cached_offset := mload(0x40)
    mcopy(cached_offset, storageSlot, 0x20)  // Cache storage value

    // Use cached_offset instead of SLOAD
}

Memory Consolidation

assembly {
    // Compact memory layout
    let src1 := 0x100
    let src2 := 0x200
    let dst := mload(0x40)

    mcopy(dst, src1, 0x20)           // Copy first chunk
    mcopy(add(dst, 0x20), src2, 0x20) // Copy second chunk

    mstore(0x40, add(dst, 0x40))     // Update free pointer
}

Memory-to-Memory Transfer

assembly {
    // Efficient data transfer between memory regions
    let length := mload(sourceAddr)  // Get length prefix

    // Copy length + data
    mcopy(destAddr, sourceAddr, add(0x20, length))
}

Memory Safety

Copy safety properties:
  • Atomic: Entire copy completes without intermediate states visible
  • Non-destructive for overlaps: Uses temporary buffer internally
  • Initialization: Uninitialized source reads as zero
  • No side effects: Doesn’t affect storage or state
Memory layout considerations:
// Good: Managed memory regions
assembly {
    let region1 := mload(0x40)
    let region2 := add(region1, 0x100)

    mcopy(region2, region1, 0x50)  // Safe, non-overlapping
    mstore(0x40, add(region2, 0x50))
}

// Risky: Overlapping regions without buffer
assembly {
    mcopy(0x00, 0x10, 0x20)  // Forward overlap (but handled correctly)
}

Implementation

/**
 * MCOPY opcode (0x5e) - Copy memory (Cancun+, EIP-5656)
 */
export function mcopy(frame: FrameType): EvmError | null {
  // Check Cancun availability
  if (frame.evm.hardfork.isBefore('CANCUN')) {
    return { type: "InvalidOpcode" };
  }

  // Pop stack values (dest, src, len)
  if (frame.stack.length < 3) {
    return { type: "StackUnderflow" };
  }
  const dest = frame.stack.pop();
  const src = frame.stack.pop();
  const len = frame.stack.pop();

  // Cast to u32
  const destNum = Number(dest);
  const srcNum = Number(src);
  const lenNum = Number(len);

  if (!Number.isSafeInteger(destNum) || !Number.isSafeInteger(srcNum) ||
      !Number.isSafeInteger(lenNum) || destNum < 0 || srcNum < 0 || lenNum < 0) {
    return { type: "OutOfBounds" };
  }

  // Zero-length copy: only base gas
  if (lenNum === 0) {
    frame.gasRemaining -= 3n;
    if (frame.gasRemaining < 0n) {
      return { type: "OutOfGas" };
    }
    frame.pc += 1;
    return null;
  }

  // Calculate memory expansion for both ranges
  const maxEnd = Math.max(destNum + lenNum, srcNum + lenNum);
  const expansionCost = calculateMemoryExpansion(maxEnd);

  // Calculate copy cost (3 gas per word)
  const copyWords = Math.ceil(lenNum / 32);
  const copyCost = copyWords * 3;

  // Total gas
  const totalGas = 3n + BigInt(expansionCost) + BigInt(copyCost);
  frame.gasRemaining -= totalGas;
  if (frame.gasRemaining < 0n) {
    return { type: "OutOfGas" };
  }

  // Expand memory
  const alignedSize = Math.ceil(maxEnd / 32) * 32;
  frame.memorySize = Math.max(frame.memorySize, alignedSize);

  // Copy using temporary buffer to handle overlaps
  const temp = new Uint8Array(lenNum);
  for (let i = 0; i < lenNum; i++) {
    temp[i] = frame.memory.get(srcNum + i) ?? 0;
  }
  for (let i = 0; i < lenNum; i++) {
    frame.memory.set(destNum + i, temp[i]);
  }

  frame.pc += 1;
  return null;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { mcopy } from './0x5e_MCOPY.js';

describe('MCOPY (0x5e)', () => {
  it('copies memory from source to destination', () => {
    const frame = createFrame();

    for (let i = 0; i < 32; i++) {
      frame.memory.set(i, i + 1);
    }

    frame.stack.push(32n);  // len
    frame.stack.push(0n);   // src
    frame.stack.push(64n);  // dest

    expect(mcopy(frame)).toBeNull();

    for (let i = 0; i < 32; i++) {
      expect(frame.memory.get(64 + i)).toBe(i + 1);
    }
    expect(frame.pc).toBe(1);
  });

  it('handles zero-length copy', () => {
    const frame = createFrame();

    frame.stack.push(0n);   // len = 0
    frame.stack.push(0n);   // src
    frame.stack.push(0n);   // dest

    expect(mcopy(frame)).toBeNull();

    expect(frame.memorySize).toBe(0);  // No expansion
    expect(frame.gasRemaining).toBe(999997n);  // Only base gas
  });

  it('handles forward overlap correctly', () => {
    const frame = createFrame();

    for (let i = 0; i < 64; i++) {
      frame.memory.set(i, i);
    }

    frame.stack.push(32n);  // len
    frame.stack.push(0n);   // src
    frame.stack.push(16n);  // dest (overlap)

    expect(mcopy(frame)).toBeNull();

    for (let i = 0; i < 32; i++) {
      expect(frame.memory.get(16 + i)).toBe(i);
    }
  });

  it('handles backward overlap correctly', () => {
    const frame = createFrame();

    for (let i = 0; i < 64; i++) {
      frame.memory.set(16 + i, i + 100);
    }

    frame.stack.push(32n);  // len
    frame.stack.push(16n);  // src
    frame.stack.push(0n);   // dest (backward overlap)

    expect(mcopy(frame)).toBeNull();

    for (let i = 0; i < 32; i++) {
      expect(frame.memory.get(i)).toBe(i + 100);
    }
  });

  it('charges correct gas for copy', () => {
    const frame = createFrame({ gasRemaining: 1000n, memorySize: 128 });

    frame.stack.push(32n);  // 1 word
    frame.stack.push(0n);
    frame.stack.push(64n);

    expect(mcopy(frame)).toBeNull();

    // Base: 3, Copy: 1 word * 3 = 3
    expect(frame.gasRemaining).toBe(994n);
  });

  it('returns InvalidOpcode before Cancun', () => {
    const frame = createFrame();
    frame.evm.hardfork = 'Shanghai';

    frame.stack.push(32n);
    frame.stack.push(0n);
    frame.stack.push(0n);

    expect(mcopy(frame)).toEqual({ type: "InvalidOpcode" });
  });

  it('returns OutOfGas when insufficient', () => {
    const frame = createFrame({ gasRemaining: 2n });

    frame.stack.push(32n);
    frame.stack.push(0n);
    frame.stack.push(0n);

    expect(mcopy(frame)).toEqual({ type: "OutOfGas" });
  });

  it('returns StackUnderflow with only 2 items', () => {
    const frame = createFrame();
    frame.stack.push(0n);
    frame.stack.push(0n);

    expect(mcopy(frame)).toEqual({ type: "StackUnderflow" });
  });
});

Edge Cases Tested

  • Basic copy (32 bytes)
  • Zero-length copy
  • Forward overlap
  • Backward overlap
  • Exact overlap (src == dest)
  • Uninitialized source (zeros)
  • Memory expansion
  • Copy cost calculation
  • Large copies (256+ bytes)
  • Hardfork checking
  • Stack underflow/overflow
  • Out of gas conditions

Security Considerations

Overlap Handling

MCOPY correctly handles all overlap scenarios with internal buffering:
// Safe: Overlapping regions
assembly {
    // Bytes 0-63 contain source pattern
    mcopy(32, 0, 64)  // Copy bytes 0-63 to 32-95 (forward overlap)

    // Result: bytes 0-31 unchanged, bytes 32-95 contain copy
}

Memory Exhaustion

Large copies can trigger quadratic memory expansion costs:
// Expensive: Very large copy
assembly {
    mcopy(1000000, 0, 1000000)  // Quadratic gas cost
}

// Better: Validate size before copying
require(len < maxSize, "copy too large");
mcopy(dest, src, len);

Benchmarks

MCOPY efficiency gains over manual loops: MLOAD/MSTORE loop (32 bytes):
6 ops × 3 gas = 18 gas (base only, no expansion)
MCOPY (32 bytes):
3 (base) + 0 (expansion) + 3 (copy) = 6 gas
3x more efficient for single-word copies. Gains increase for larger copies due to reduced opcode overhead.

References