Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x5E |
| Mnemonic | MCOPY |
| Gas | 3 + 3 × ceil(len / 32) + memory expansion cost |
| Stack Input | dst, src, len |
| Stack Output | (none) |
| Behavior | Copies len bytes of memory from offset src to offset dst. Handles overlapping source and destination regions correctly, behaving like C’s memmove rather than memcpy. If len > 0 and either src + len or dst + len exceeds the current memory size, memory is expanded with the standard quadratic gas cost applied. When len = 0, no memory expansion occurs and the operation is a no-op regardless of src and dst values. Introduced in the Cancun hard fork (March 2024) via EIP-5656. Part of the W_copy gas group alongside CALLDATACOPY, CODECOPY, and RETURNDATACOPY. Stack ordering matches those opcodes (destination first). |
Threat Surface
MCOPY is a new opcode activated on Ethereum mainnet on March 13, 2024, as part of the Cancun (Dencun) upgrade. It provides a native EVM instruction for memory-to-memory copying, replacing inefficient patterns that previously required unrolled MLOAD/MSTORE loops (96+ gas for 256 bytes) or the identity precompile (157+ gas post-EIP-2929). MCOPY performs the same 256-byte copy for 27 gas.
The threat surface centers on four properties:
-
New opcode with limited battle-testing. MCOPY has been live on mainnet for approximately two years. While the Ethereum execution spec test suite covers it extensively and all major clients passed the Cancun test vectors, the opcode has seen far less adversarial usage than established memory opcodes. Any implementation subtlety — particularly around overlapping copies, gas accounting at memory boundaries, and interaction with the quadratic memory cost model — represents potential consensus divergence risk across clients. The Scroll zkEVM audit (Zellic, 2023) already demonstrated that MCOPY’s copy circuit can be incorrectly constrained, and its gas cost miscalculated in error-handling paths.
-
Overlapping memory copies introduce implementation complexity. MCOPY must behave like
memmove: whensrcanddstoverlap, the result must be as if the data was first copied to an intermediate buffer and then written to the destination. EIP-5656’s security considerations explicitly warn that client implementations must not actually use an intermediate buffer (DoS vector via memory allocation), but must instead use directional copying (forward or backward depending on overlap direction). This is a correctness requirement that is easy to get wrong in novel EVM implementations (zkEVMs, alternative clients, embedded interpreters). -
Memory expansion gas remains a griefing vector. MCOPY triggers the same quadratic memory expansion cost as MLOAD, MSTORE, and other memory-touching opcodes. An attacker can invoke MCOPY with a large
dst + lenorsrc + lenvalue to force memory expansion in a caller’s execution context. While this is not unique to MCOPY, the opcode makes memory expansion more accessible from assembly because it can expand memory at both the source and destination offsets in a single instruction — unlike MLOAD (one offset) or MSTORE (one offset). -
Behavioral differences when replacing manual copy loops. Solidity 0.8.25+ uses MCOPY for byte array copying in generated code. Contracts compiled with MCOPY-aware compilers behave identically in terms of semantics, but gas profiles change. More critically, hand-written assembly that replaces MLOAD/MSTORE loops with MCOPY must account for the overlap-safe semantics: a forward MLOAD/MSTORE loop that previously corrupted overlapping regions now produces correct results with MCOPY, which can change contract behavior in subtle ways if the corruption was load-bearing (e.g., a pseudo-random shuffle that relied on overlap corruption as an entropy source).
Smart Contract Threats
T1: Memory Expansion Gas Griefing (High)
MCOPY triggers memory expansion at both src + len and dst + len. EVM memory expansion cost is quadratic:
memory_cost = (words² / 512) + (3 × words)
An attacker who can influence MCOPY’s dst or len parameters — directly through user-supplied calldata in assembly blocks, or indirectly through data structures that control copy offsets — can force the caller to pay quadratic gas costs for memory expansion.
-
Dual-offset expansion. Unlike MLOAD or MSTORE, which expand memory at a single offset, MCOPY expands memory at both
src + lenanddst + len. If an attacker controlssrcto be a very high offset anddstto be another high offset, the memory expansion cost accounts for the maximum of both, potentially doubling the expected expansion in scenarios where only one offset was considered by the developer. -
Griefing in delegated calls. When a contract uses DELEGATECALL to invoke a library that performs MCOPY operations, the memory expansion occurs in the caller’s context. A malicious library (or a library called with attacker-controlled parameters) can expand memory far beyond what the caller anticipated, consuming gas that the caller reserved for subsequent operations.
-
Assembly-level parameter injection. Contracts that accept user-provided offsets or lengths for memory operations in inline assembly (e.g., custom ABI decoders, buffer manipulation libraries) are directly vulnerable if they pass unvalidated values to MCOPY.
Why it matters: Memory expansion is the primary gas griefing vector for all memory-touching opcodes, and MCOPY’s dual-offset expansion makes it marginally more dangerous. Any protocol where gas exhaustion causes a revert on a critical path (liquidations, unstaking, governance execution) can be DoS’d through MCOPY-triggered memory expansion.
T2: Overlapping Copy Edge Cases (Medium)
MCOPY handles overlapping regions correctly by specification, but this correctness depends on every EVM implementation getting the directional copy logic right. The threat is not in MCOPY itself but in the assumptions developers make about overlap behavior:
-
Forward overlap (dst > src, regions overlap). Bytes are copied backward to avoid overwriting source data before it is read. A developer who previously used a forward MLOAD/MSTORE loop for this case would have seen corruption; switching to MCOPY silently fixes it. If the corruption was expected (however pathological), behavior changes.
-
Backward overlap (dst < src, regions overlap). Bytes are copied forward. Same correctness guarantee. The concern is symmetrical.
-
Complete overlap (dst == src). MCOPY with
dst == srcis a no-op in terms of memory content but still charges gas (3 + 3 × ceil(len/32)) and triggers memory expansion ifsrc + lenexceeds current memory size. A contract that unconditionally calls MCOPY without checkingdst == srcwastes gas on a semantically empty operation. -
zkEVM circuit correctness. The Scroll zkEVM audit by Zellic found that the copy circuit for MCOPY had missing constraints, allowing illegitimate entries in the read-write table. While this is a zkEVM-specific issue, it demonstrates that overlap handling is a correctness surface that alternative EVM implementations can get wrong.
Why it matters: Overlap correctness is a specification invariant, not a security feature. But any deviation between implementations (mainnet clients, L2 clients, zkEVM provers) is a consensus bug that could be exploited for double-spends or state divergence.
T3: Replacing Manual Copy Loops — Behavioral Differences (Medium)
Solidity 0.8.25+ generates MCOPY for byte array copies when targeting the Cancun EVM version. This affects:
-
Gas profile changes. Contracts recompiled with Solidity 0.8.25+ will consume different (generally less) gas for memory copy operations. Gas-dependent logic (e.g.,
gasleft()checks, gas estimation for meta-transactions, relayer reimbursement calculations) may break if calibrated against pre-MCOPY gas costs. -
Pre-Cancun deployment targets. If a contract is compiled targeting a pre-Cancun EVM version but deployed on a post-Cancun chain, MCOPY will not be used (the compiler emits MLOAD/MSTORE loops instead). This is correct behavior, but developers who expect MCOPY gas savings may be surprised.
-
Hand-rolled assembly replacements. Developers replacing MLOAD/MSTORE assembly loops with a single MCOPY instruction must verify that:
- The overlap semantics match their intent (MCOPY is memmove; manual loops are typically memcpy).
- The memory expansion behavior is equivalent (MCOPY expands at both src and dst; a loop may only touch one side at a time, with intermediate expansion steps).
- Edge cases like
len = 0are handled (MCOPY no-ops; a loop withlen = 0may still execute once depending on loop structure).
Why it matters: Compiler-level adoption of MCOPY is the primary vector through which this opcode enters production contracts. Subtle gas and semantic differences between MCOPY and the code it replaces create regression risk during recompilation.
T4: Assembly-Level Misuse (Medium)
MCOPY is available in Solidity inline assembly (Yul) as mcopy(dst, src, len). Misuse patterns include:
-
Unbounded length from calldata.
mcopy(dst, src, calldataload(offset))where the length is read directly from calldata without validation. An attacker submits a transaction withlen = 2^128, causing memory expansion to exceed the block gas limit and reverting the transaction. -
Incorrect parameter ordering. MCOPY’s stack order is
dst, src, len(matching CALLDATACOPY and RETURNDATACOPY). Developers accustomed to C’smemcpy(dst, src, len)will find this natural, but confusion with MLOAD/MSTORE patterns (which take a single offset) can lead to swappeddstandsrcin complex assembly blocks, silently corrupting data without reverting. -
Using MCOPY where CALLDATACOPY should be used. MCOPY copies memory-to-memory. Developers who want to copy calldata to memory should use CALLDATACOPY. Using MCOPY on a region that has not been populated from calldata will copy whatever is in memory (potentially zeroes or stale data from a previous operation).
Why it matters: MCOPY is primarily used in assembly. Assembly code lacks Solidity’s safety checks, and parameter ordering or source confusion bugs produce silent data corruption rather than reverts.
T5: Length = 0 Behavior and Gas Waste (Low)
When len = 0, MCOPY is a no-op: no bytes are copied, no memory expansion occurs, and the gas cost is 3 (the g_verylow base cost only, since ceil(0/32) = 0). This differs from RETURNDATACOPY, which reverts on out-of-bounds offsets even with len = 0.
-
Gas waste in loops. A contract that iterates over a data structure and calls MCOPY for each element will pay 3 gas per zero-length call. In tight loops processing thousands of elements where many have zero length, this adds up.
-
No revert on invalid offsets. When
len = 0, MCOPY does not revert regardless ofsrcanddstvalues — even if they are2^256 - 1. This meanslen = 0cannot be used as a boundary check. Developers who expect MCOPY to revert on “impossible” offsets when length is zero will be surprised. -
Contrast with RETURNDATACOPY. RETURNDATACOPY reverts if
offset > RETURNDATASIZEeven withlen = 0(per EIP-211). MCOPY has no such bounds check because EVM memory is unbounded (limited only by gas). Developers porting patterns from RETURNDATACOPY to MCOPY must not assume equivalent boundary checking.
Why it matters: The len = 0 edge case is well-specified and safe, but the behavioral difference from RETURNDATACOPY’s len = 0 revert is a knowledge gap that can lead to missing validation.
Protocol-Level Threats
P1: New Opcode Implementation Bugs (Medium)
MCOPY was introduced in Cancun (March 2024) and required implementation across all major execution clients: Geth, Nethermind, Besu, Erigon, and Reth. The overlap-safe copying semantics add complexity beyond simpler opcodes:
-
Directional copy requirement. EIP-5656 specifies that MCOPY behaves “as if an intermediate buffer was used” for overlapping regions, but explicitly warns against actually allocating an intermediate buffer (DoS vector). Each client must implement directional copying (forward for
dst < src, backward fordst > src). A client that implements a naive forward-only copy will produce incorrect results fordst > srcoverlapping regions, which is a consensus-critical bug. -
Gas calculation at memory boundaries. MCOPY’s gas cost includes memory expansion for both
src + lenanddst + len. A client that only calculates expansion for the destination (as one might do for MSTORE) will undercharge gas, potentially enabling block-stuffing attacks that exploit the gas mispricing. -
Geth PR #26181. Go-Ethereum’s implementation adapted the
opCallDataCopypattern and uses Go’s built-incopy()function, which handles overlaps correctly. Other clients may use different underlying primitives with different overlap guarantees.
P2: Client Divergence in zkEVM and L2 Implementations (High)
The Scroll zkEVM audit by Zellic (September 2023) revealed two MCOPY-specific vulnerabilities:
-
Critical: Missing copy circuit constraints. The copy circuit for MCOPY lacked constraints that should prevent illegitimate entries from being inserted into the read-write (rw) table. A malicious prover could exploit this to create valid proofs for false memory copy operations, potentially proving state transitions that never occurred.
-
Medium: Incorrect gas cost in ErrorOOGMemoryCopyGadget. The gas cost calculation in the out-of-gas error handling gadget did not match the Ethereum specification. The
MCopyGadgetcorrectly accounted for memory expansion at both source and destination addresses, but theErrorOOGMemoryCopyGadgetonly considered the destination address. This meant certain out-of-gas conditions were not correctly detected, potentially allowing transactions that should have failed to be proven as successful.
These findings are zkEVM-specific, but they illustrate a general pattern: MCOPY’s dual-offset memory expansion and overlap semantics are more complex than existing copy opcodes, and this complexity creates implementation divergence risk in any non-reference EVM implementation.
Why it matters: L2s running custom EVM implementations (zkEVMs, optimistic rollup VMs) may have MCOPY bugs that do not exist on L1 mainnet. Any state divergence between the L2 prover and the L1 verification contract is exploitable for fraudulent withdrawals.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
len = 0 | No-op. No memory expansion, no bytes copied. Gas cost is 3 (base cost only). Does not revert regardless of src or dst values. | Cannot be used as a boundary check. Differs from RETURNDATACOPY which reverts on OOB offset even with len = 0. |
src and dst overlap (dst > src) | Copies backward to preserve source data. Result is identical to copying through an intermediate buffer. | Correct by specification, but implementation complexity. zkEVM circuits have gotten this wrong (Zellic/Scroll finding). |
src and dst overlap (dst < src) | Copies forward. Same intermediate-buffer semantics. | Same implementation risk as above. Forward copy is the simpler case but must be explicitly chosen based on overlap direction. |
dst == src | Semantically a no-op (memory content unchanged), but gas is still charged: 3 + 3 × ceil(len/32) plus any memory expansion cost. | Gas waste if not short-circuited. Memory expansion still occurs if src + len exceeds current memory size. |
Very large len (e.g., 2^128) | Memory expansion cost exceeds block gas limit. Transaction reverts with out-of-gas. | Standard memory expansion DoS. The quadratic cost formula prevents actual allocation, but the gas is consumed before the revert. |
dst + len overflows uint256 | EVM treats offsets as 256-bit unsigned integers. Overflow in dst + len or src + len causes the memory expansion calculation to reference an astronomically large offset, immediately exceeding the block gas limit. | Reverts with out-of-gas. No security issue per se, but assembly code that computes offsets without overflow checks will fail silently. |
src + len exceeds current memory, dst is within | Memory is expanded to accommodate src + len. The newly expanded source region is zero-initialized (EVM memory is zero by default). Zeros are copied to dst. | Reads from uninitialized memory return zero, not garbage. Safe, but may produce unexpected results if the developer assumed the source was populated. |
| MCOPY in STATICCALL context | MCOPY only modifies memory (call-frame local), not storage. Fully permitted in STATICCALL. | No state mutation concern. MCOPY is safe in read-only contexts. |
| MCOPY in DELEGATECALL context | Operates on the caller’s memory (as with all memory operations in DELEGATECALL). | A delegated library can expand the caller’s memory and copy data within it. If the library is malicious, this can corrupt the caller’s memory layout. |
| Pre-Cancun deployment | MCOPY (0x5E) is an invalid opcode before the Cancun fork. Execution reverts with an invalid opcode error. | Contracts compiled for Cancun and deployed on pre-Cancun chains or L2s that haven’t activated Cancun will fail at MCOPY instructions. |
Real-World Exploits
Exploit 1: Scroll zkEVM — Critical Missing Constraints in MCOPY Copy Circuit (Zellic Audit, September 2023)
Root cause: The Scroll zkEVM’s copy circuit for MCOPY lacked constraints that should have prevented illegitimate entries from being inserted into the read-write (rw) table, allowing a malicious prover to forge memory state transitions.
Details: During the pre-Cancun audit of Scroll’s zkEVM circuits, Zellic identified that the copy circuit handling MCOPY operations did not properly constrain which entries could be added to the rw table. The rw table is the core data structure in zkEVM circuit design that tracks all memory reads and writes — it is the source of truth for proving that state transitions are correct.
The missing constraints meant that a malicious prover could insert entries into the rw table that did not correspond to actual MCOPY operations. This would allow the prover to generate a valid zero-knowledge proof for a block containing fabricated memory copy results — effectively proving that a contract’s memory contained values that were never actually written there.
Additionally, the ErrorOOGMemoryCopyGadget calculated MCOPY’s gas cost incorrectly: it only considered memory expansion at the destination address, ignoring the source address. This meant that an MCOPY operation that should have reverted with out-of-gas (because src + len triggered expensive memory expansion) could be proven as successful, potentially allowing transactions to execute with less gas than the specification requires.
MCOPY’s role: The vulnerability was specific to MCOPY’s dual-offset memory expansion model. Existing copy opcodes (CALLDATACOPY, CODECOPY, RETURNDATACOPY) copy from non-memory sources, so their copy circuits only need to constrain writes to a single memory region. MCOPY reads from and writes to memory, requiring constraints on both source reads and destination writes in the rw table. The additional complexity of constraining both sides was where the bug emerged.
Impact: Critical severity in the zkEVM context — a malicious prover could forge state transitions, potentially enabling theft of bridged funds if the circuit were deployed without the fix. Identified and fixed before Scroll’s Curie upgrade activated MCOPY on mainnet.
References:
- Zellic Scroll zkEVM Audit: MCOPY Copy Circuit Finding
- Zellic Scroll zkEVM Audit: Assessment Results
- Scroll Audit Repository
Exploit 2: No Public Mainnet MCOPY Exploit to Date (As of March 2026)
Status: As of March 2026, no publicly disclosed exploit on Ethereum mainnet or major L2s has been attributed to the MCOPY opcode. This is notable and expected for several reasons:
-
MCOPY is semantically simple. Unlike RETURNDATACOPY (which has the revert-on-OOB asymmetry) or DELEGATECALL (which has context-switching subtleties), MCOPY performs a straightforward memory-to-memory copy with well-defined overlap semantics. There is no external data dependency, no revert condition beyond out-of-gas, and no interaction with storage or external contracts.
-
MCOPY replaces more dangerous patterns. The code it replaces — MLOAD/MSTORE loops and identity precompile calls via CALL — had larger attack surfaces. MLOAD/MSTORE loops don’t handle overlaps correctly, and the identity precompile involves a full CALL with its associated gas forwarding and return data handling. MCOPY eliminates both of these risk surfaces.
-
Compiler adoption limits direct exposure. Most contracts interact with MCOPY through Solidity’s compiler (0.8.25+), not through hand-written assembly. The compiler generates correct MCOPY usage, limiting the exposure to assembly-level misuse patterns.
However, the absence of exploits does not indicate absence of risk. MCOPY is two years old on mainnet. The Scroll zkEVM circuit bug demonstrates that implementation-level vulnerabilities exist, and the memory expansion gas model remains a griefing vector. As MCOPY adoption increases in complex assembly-heavy protocols (DEX aggregators, MEV bots, data processing contracts), the attack surface will grow.
Attack Scenarios
Scenario A: Memory Expansion Gas Griefing via MCOPY
contract VulnerableBuffer {
function processData(bytes calldata input) external {
assembly {
let len := calldataload(add(input.offset, 0))
let dstOffset := calldataload(add(input.offset, 32))
// Copy calldata into memory first
calldatacopy(0x80, input.offset, calldatasize())
// VULNERABLE: len and dstOffset are user-controlled.
// Attacker sets dstOffset = 0x1000000 (16 MB offset).
// Memory expansion to 16 MB costs ~500M gas (quadratic),
// exceeding the block gas limit and reverting the transaction.
mcopy(dstOffset, 0x80, len)
}
}
}
// Attack: Submit calldata with dstOffset = 0x1000000.
// The MCOPY triggers memory expansion to 16 MB.
// Gas cost: (524288² / 512) + (3 × 524288) ≈ 537M gas.
// Block gas limit is 30M -> transaction reverts with OOG.
// If this function is on a critical path (e.g., unstaking), it's a DoS.Scenario B: Overlapping Copy Behavioral Change
contract OverlapDependency {
// Before MCOPY: manual loop with forward copy
function legacyShuffle(uint256[] memory data) internal pure {
assembly {
let len := mload(data)
let src := add(data, 0x20)
let dst := add(src, 0x20) // dst = src + 32 (overlapping)
let size := mul(sub(len, 1), 0x20)
// Forward MLOAD/MSTORE loop: each write overwrites the
// next iteration's source, creating a "smearing" effect
// where the first element propagates through the array.
for { let i := 0 } lt(i, size) { i := add(i, 0x20) } {
mstore(add(dst, i), mload(add(src, i)))
}
// Result with [A, B, C, D]: [A, A, A, A] (all smeared to first)
}
}
// After MCOPY: same operation, different result
function mcopyVersion(uint256[] memory data) internal pure {
assembly {
let len := mload(data)
let src := add(data, 0x20)
let dst := add(src, 0x20)
let size := mul(sub(len, 1), 0x20)
// MCOPY handles overlap correctly (memmove semantics).
// No smearing -- data is shifted correctly.
mcopy(dst, src, size)
// Result with [A, B, C, D]: [A, A, B, C] (shifted right)
}
}
// If a contract relied on the "smearing" behavior for any logic
// (e.g., clearing an array, pseudo-randomization), replacing the
// loop with MCOPY silently changes the output.
}Scenario C: Assembly Parameter Ordering Bug
contract ParameterConfusion {
function copyRegion(
uint256 dst,
uint256 src,
uint256 len
) external pure returns (bytes memory result) {
result = new bytes(len);
assembly {
// Store source data in memory
mstore(0x100, 0xDEADBEEF)
mstore(0x120, 0xCAFEBABE)
// BUG: Developer swapped src and dst.
// Intended: copy from 0x100 to result buffer
// Actual: copy from result buffer (zeroes) to 0x100,
// overwriting the source data with zeroes.
mcopy(0x100, add(result, 0x20), len) // Swapped!
// Should be: mcopy(add(result, 0x20), 0x100, len)
}
// result is all zeroes; source data at 0x100 is destroyed.
// No revert, no error -- silent data corruption.
}
}Scenario D: Delegatecall Memory Corruption
contract TrustedProxy {
uint256 private secretNonce;
function execute(address target, bytes calldata data) external {
// Store sensitive data in known memory location
assembly {
mstore(0x40, sload(secretNonce.slot))
}
// DELEGATECALL: target's code runs in this contract's context,
// including this contract's memory space.
(bool ok,) = target.delegatecall(data);
require(ok);
// Read back the nonce from memory
uint256 nonce;
assembly {
nonce := mload(0x40)
}
// If the target library used MCOPY to rearrange memory,
// it may have overwritten 0x40 (the free memory pointer region).
// The nonce value is now corrupted without any revert.
_useNonce(nonce);
}
function _useNonce(uint256 n) internal { /* ... */ }
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Memory expansion gas griefing | Validate all MCOPY offset and length parameters against bounded maximums | require(dst + len <= MAX_MEMORY && src + len <= MAX_MEMORY) or equivalent assembly bounds check before mcopy |
| T1: Dual-offset expansion cost | Calculate memory expansion for both src + len and dst + len when estimating gas | Gas estimation tools must account for the max of both offsets when computing expansion cost |
| T2: Overlapping copy correctness | Test overlap cases explicitly in unit tests and fuzz tests | Test with dst < src, dst > src, dst == src, and full/partial overlap ranges |
| T2: zkEVM circuit correctness | Audit copy circuits for MCOPY separately from other copy opcodes | Ensure rw table constraints cover both source reads and destination writes |
| T3: Behavioral changes from recompilation | Pin compiler version or verify gas profiles after recompilation | Run gas snapshot tests before and after upgrading Solidity to 0.8.25+; audit assembly blocks that replace MLOAD/MSTORE with MCOPY |
| T4: Parameter ordering (dst/src swap) | Use named parameters in Yul or document parameter order at every MCOPY call site | Comment // mcopy(dst, src, len) at every usage; consider wrapper functions: function safeCopy(dst, src, len) |
| T4: Unbounded length from calldata | Cap MCOPY length to a protocol-specific maximum | if gt(len, MAX_COPY_LEN) { revert(0, 0) } before mcopy(dst, src, len) |
T5: Wasted gas on len = 0 | Short-circuit MCOPY calls when length is known to be zero | if iszero(len) { /* skip mcopy */ } in assembly; compiler handles this for high-level code |
| General: Pre-Cancun chain compatibility | Check block.chainid or use compiler EVM version targeting to avoid MCOPY on unsupported chains | Solidity pragma with correct EVM target; do not compile with --evm-version cancun for pre-Cancun deployments |
Compiler/EIP-Based Protections
- Solidity 0.8.25+ MCOPY codegen: The Solidity compiler (PR #14834) uses MCOPY for byte array copying when targeting Cancun or later. The compiler handles overlap, parameter ordering, and memory expansion correctly in generated code. The primary risk is in hand-written assembly, not compiler-generated MCOPY.
- Yul proto fuzzer (Solidity PR #14787): The Solidity team added MCOPY to the Yul proto fuzzer in January 2024, enabling automated detection of MCOPY-related compiler bugs through randomized testing.
- Ethereum Execution Spec Tests: Comprehensive test suite covering MCOPY memory expansion, overlap, context isolation (CALL, DELEGATECALL, STATICCALL, CREATE, CREATE2), and gas edge cases. All mainnet clients must pass these tests.
- EIP-5656 Security Consideration: The EIP explicitly warns against intermediate buffer implementations (DoS via memory allocation) and mandates
memmove-equivalent semantics, providing a clear specification for auditors to verify against. - EIP-7686 (proposed): Proposes replacing the quadratic memory cost model with a linear model plus hard memory limits. If adopted, this would eliminate the quadratic memory expansion griefing vector that affects MCOPY and all other memory-touching opcodes.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | Medium | Memory expansion griefing is a known class (identical to MSTORE/CALLDATACOPY vectors); EIP-7686 proposed to address |
| T2 | Smart Contract | Medium | Low | Scroll zkEVM copy circuit bug (Zellic audit, 2023) |
| T3 | Smart Contract | Medium | Medium | Gas profile changes from Solidity 0.8.25+ recompilation |
| T4 | Smart Contract | Medium | Medium | No specific MCOPY exploit, but assembly parameter confusion is a pervasive bug class |
| T5 | Smart Contract | Low | Low | Well-specified no-op behavior; risk limited to gas waste and missing validation |
| P1 | Protocol | Medium | Low | No mainnet consensus bug from MCOPY; Geth/Nethermind/Besu all passed execution spec tests |
| P2 | Protocol | High | Medium | Scroll zkEVM critical finding (missing copy circuit constraints); medium finding (incorrect gas in OOG gadget) |
Related Opcodes
| Opcode | Relationship |
|---|---|
| MLOAD (0x51) | Loads 32 bytes from memory. Before MCOPY, memory copying required MLOAD/MSTORE loops. MCOPY replaces these loops with a single instruction, eliminating forward-copy overlap corruption and reducing gas cost. |
| MSTORE (0x52) | Stores 32 bytes to memory. The MLOAD/MSTORE pair was the primary memory copy mechanism before MCOPY. Manual loops using these opcodes do not handle overlapping regions correctly (forward copy corrupts data when dst > src). |
| CALLDATACOPY (0x37) | Copies calldata to memory. Same W_copy gas group and stack ordering (dst, offset, len) as MCOPY. CALLDATACOPY copies from calldata (read-only, external input); MCOPY copies from memory (read-write, internal state). |
| CODECOPY (0x39) | Copies contract code to memory. Same W_copy gas group. Zero-pads out-of-bounds reads. MCOPY differs in that out-of-bounds source reads return zero-initialized memory rather than code bytes. |
| RETURNDATACOPY (0x3E) | Copies return data to memory. Same W_copy gas group and stack ordering. Critical difference: RETURNDATACOPY reverts on out-of-bounds reads (including len = 0 with offset > RETURNDATASIZE), while MCOPY never reverts on offset values (only on out-of-gas from memory expansion). |