Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x37 |
| Mnemonic | CALLDATACOPY |
| Gas | 3 + 3 × ⌈len / 32⌉ + memory_expansion_cost |
| Stack Input | dstOst, ost, len |
| Stack Output | (none) |
| Behavior | Copies len bytes of calldata starting at byte offset ost into memory at byte offset dstOst. Source bytes beyond the actual calldata length are zero-padded. Memory is expanded (and charged) if dstOst + len exceeds the current memory size. |
Threat Surface
CALLDATACOPY is the EVM’s primary mechanism for bulk-transferring transaction input data into memory. It is foundational to proxy forwarding (EIP-1167 minimal proxies, UUPS, Transparent proxies), ABI decoding of dynamic types (bytes, string, arrays), and any contract that processes raw calldata in assembly. Its threat surface spans three main areas:
-
Dynamic gas cost (memory expansion + copy cost): Unlike fixed-cost opcodes, CALLDATACOPY’s gas depends on two user-influenced variables: the copy length (linear component: 3 gas per 32-byte word) and the destination offset (memory expansion component: quadratic beyond current memory size). An attacker who controls either
lenordstOstvia crafted calldata can force arbitrarily expensive memory expansion, griefing relayers, meta-transaction executors, or any contract that forwards gas to a sub-call containing CALLDATACOPY. -
Zero-padding of out-of-bounds reads: When
ost + lenexceedscalldatasize, the EVM silently pads the missing bytes with zeros instead of reverting. This is by design but dangerous: contracts that rely on calldata content without checkingcalldatasizemay process attacker-controlled zero bytes as valid data. In proxy patterns, zero-padded forwarded calldata can corrupt function arguments passed to the implementation contract. -
Assembly-level memory manipulation: CALLDATACOPY is almost exclusively used inside
assembly {}blocks, where Solidity’s type safety, overflow protection, and bounds checking do not apply. The destination offset, source offset, and length are all raw uint256 values. Arithmetic on these values (particularly when computing write positions from user-supplied offsets) can overflow silently, enabling arbitrary memory writes. This is the root cause of the $5M 1inch Fusion v1 exploit.
The combination of user-influenced gas costs, silent zero-padding, and assembly-context usage makes CALLDATACOPY one of the most security-sensitive data-movement opcodes.
Smart Contract Threats
T1: Memory Expansion Gas Griefing (High)
CALLDATACOPY charges memory expansion cost when the destination range (dstOst + len) exceeds the current memory size. Memory expansion cost is quadratic: extending memory to L words costs ⌊L² / 512⌋ + 3L gas. An attacker who controls len or dstOst can force massive memory allocation:
contract VulnerableForwarder {
function forward(address target) external {
assembly {
// Copies ALL calldata to memory starting at 0x0
// Attacker can send enormous calldata (up to ~3.9 MB with 30M gas)
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), target, 0, calldatasize(), 0, 0)
}
}
}With 30 million gas, an attacker can expand memory to ~3.9 MB in a single call frame. If the contract forwards a fixed gas stipend to a subsequent call, the memory expansion can consume nearly all of it, causing the sub-call to fail. This is especially dangerous for:
- Meta-transaction relayers that pay gas on behalf of users
- Keeper/resolver contracts that execute batched operations
- Proxy contracts that forward arbitrary calldata without length caps
T2: Incorrect Offset/Length in Proxy Forwarding (Critical)
Proxy contracts use CALLDATACOPY to forward the full calldata to an implementation contract. If the offset or length calculation is wrong, the forwarded data is corrupted:
assembly {
let ptr := mload(0x40)
// Bug: should be calldatasize(), not a hardcoded or computed value
calldatacopy(ptr, 0, someComputedLength)
let success := delegatecall(gas(), impl, ptr, someComputedLength, 0, 0)
}When someComputedLength is derived from user-supplied values without overflow checks, the arithmetic can wrap around. In the 1inch Fusion v1 exploit, a negative interactionLength caused an underflow in the suffix write position, allowing the attacker to overwrite the resolver address in memory and impersonate a trusted resolver.
More subtly, if the copy length is shorter than the actual calldata, trailing arguments are silently truncated. If it’s longer, zero-padded bytes are appended, which can cause the implementation to decode different argument values than the caller intended.
T3: Zero-Padding Masking Missing Calldata (High)
When a contract reads calldata beyond the actual input length, CALLDATACOPY fills the gap with zeros. This is silent — there is no revert, no flag, no way to distinguish “the caller sent 0x00” from “the caller sent nothing”:
contract VulnerableDecoder {
function decode(uint256 offset, uint256 length) external view returns (bytes memory) {
bytes memory result = new bytes(length);
assembly {
// If offset + length > calldatasize(), excess bytes are 0x00
// No revert, no indication of truncation
calldatacopy(add(result, 0x20), offset, length)
}
return result;
}
}This creates a class of bugs where contracts process zero-padded data as if it were real input:
- Missing function arguments silently default to zero (address(0), 0 amounts, false booleans)
- Dynamic arrays appear shorter than expected when calldata is truncated
- Proxy-forwarded calls may deliver zero-padded arguments to the implementation, causing it to operate on phantom data
T4: Memory Overlap and Stale Data Corruption (Medium)
CALLDATACOPY writes to an arbitrary memory region. If the destination overlaps with existing memory content (free memory pointer area, ABI-encoded return data, scratch space), the copy can corrupt unrelated data:
assembly {
let ptr := mload(0x40)
// Store important data at ptr
mstore(ptr, importantValue)
// Bug: calldatacopy overwrites ptr region
calldatacopy(ptr, 4, calldatasize())
// importantValue is now corrupted
}This is particularly dangerous in complex assembly blocks that use the same memory region for multiple purposes. The 1inch exploit leveraged exactly this pattern: CALLDATACOPY wrote calldata into a buffer, and then a subsequent write (whose position was computed from corrupted arithmetic) overwrote data at an unintended offset.
T5: Unchecked Arithmetic on User-Supplied Offsets (Critical)
Inside assembly blocks, all arithmetic on CALLDATACOPY parameters is unchecked. When dstOst, ost, or len are derived from user-supplied calldata values, overflow/underflow can redirect writes to arbitrary memory locations:
assembly {
let ptr := mload(0x40)
let userOffset := calldataload(0x24)
let userLength := calldataload(0x44)
// Intended: write to ptr + 0x24 + userOffset * 0x20
// If userOffset is very large, mul overflows and wraps to a small value
let dst := add(add(ptr, 0x24), mul(userOffset, 0x20))
calldatacopy(dst, 0x64, userLength)
// dst can point anywhere in memory, including the free memory pointer,
// function selector region, or ABI-encoded return data
}This is the exact pattern exploited in both the 1inch Fusion v1 attack and the wagmi-leverage vulnerability. In both cases, attacker-controlled index values caused arithmetic overflow, redirecting memory writes to overwrite critical data (resolver address and function selector, respectively).
Protocol-Level Threats
P1: Memory Expansion DoS at Block Level (Medium)
A single transaction using CALLDATACOPY with a large length can expand memory to ~3.9 MB, consuming up to 30 million gas (a full block). While this costs the attacker the gas fee, it can be used to:
- Fill blocks to delay time-sensitive transactions (oracle updates, liquidations)
- Grief sequencers on L2s that have lower gas limits
- Exhaust gas in contracts that process multiple CALLDATACOPY operations in a loop
P2: Consensus Safety — Historical Zero-Padding Bug (Low)
Go-ethereum issue #496 documented a bug where CALLDATACOPY did not write zeros to memory when the source offset exceeded calldata size, leaving stale memory content instead of zero-padding. This was a consensus-critical bug: different EVM implementations disagreed on the memory contents after an OOB copy. The bug has been fixed in all major clients, but any new EVM implementation must handle this edge case correctly.
P3: L2/zkEVM Compatibility (Medium)
CALLDATACOPY’s dynamic gas cost (copy cost + memory expansion) must be faithfully reproduced in L2 execution environments. zkEVM circuits must prove the correct computation of 3 × ⌈len / 32⌉ plus memory expansion. Scroll’s zkEVM audit (Zellic, 2024) identified completeness issues for out-of-gas cases involving memory-expanding opcodes including CALLDATACOPY, where the circuit failed to correctly constrain gas accounting for edge cases near the gas limit.
P4: Calldata Pricing and Future EIPs (Low)
EIP-7623 (2024) increased the effective gas cost of calldata-heavy transactions to mitigate data availability abuse. While this affects the transaction-level cost of large calldata, it does not change CALLDATACOPY’s execution cost. However, future calldata repricing could change the economics of attacks that rely on sending large calldata to trigger expensive CALLDATACOPY operations.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
len = 0 | No memory written, no gas beyond base 3 | Safe no-op; but memory expansion still charged if dstOst extends memory |
ost > calldatasize() | All copied bytes are 0x00 | Phantom zero data; contracts may process as valid input |
ost + len > calldatasize() | In-range bytes copied, remainder zero-padded | Partial data + silent zero fill; truncation not detectable |
dstOst very large | Massive memory expansion cost (quadratic) | Gas griefing; can consume entire block gas limit |
len very large | Copy cost (3 per word) + memory expansion | Combined linear + quadratic cost; DoS vector |
dstOst + len overflows uint256 | Wraps to small value; minimal memory expansion | Unexpected memory write target; potential corruption |
| Overlapping source (calldata) and dest (memory) | No conflict; calldata is read-only, memory is write-only | N/A; unlike MCOPY, source and dest are in different address spaces |
len = calldatasize(), ost = 0 | Full calldata copy (standard proxy pattern) | Safe when dstOst is controlled; dangerous if calldata is adversarially large |
| Copy to scratch space (0x00-0x3F) | Overwrites Solidity scratch space | Can corrupt intermediate computation values |
| Copy to free memory pointer (0x40) | Overwrites Solidity’s memory allocator state | Subsequent mload(0x40) returns corrupted pointer; heap corruption |
Real-World Exploits
Exploit 1: 1inch Fusion v1 Calldata Corruption — $5M Stolen (March 2025)
Root cause: Integer underflow in assembly-level offset calculation used alongside CALLDATACOPY, allowing an attacker to overwrite the resolver address in memory.
Details: The 1inch Fusion v1 Settlement contract’s _settleOrder() function used Yul assembly to parse serialized order data from calldata. The function copied calldata into memory using CALLDATACOPY, then computed write positions for a “suffix” containing the resolver address. The interactionLength field was read from calldata without validation. The attacker supplied a value of 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE00 (-512 in two’s complement), causing the suffix write position to underflow. This redirected the write to an earlier memory location, overwriting the legitimate resolver address with the attacker’s address.
The attacker crafted a payload with 6 nested orders and carefully calculated ABI encoding offsets to align the memory corruption precisely. The Settlement contract then called the forged “resolver” (the attacker’s contract), which drained approximately $5M in USDC and WETH from the TrustedVolumes resolver.
CALLDATACOPY’s role: CALLDATACOPY was used to bulk-load the malicious calldata into memory. The vulnerability was not in CALLDATACOPY itself but in the unchecked arithmetic performed on values derived from the calldata it copied. The opcode faithfully copied the attacker’s crafted payload — including the malicious negative length value — into memory, where subsequent assembly code operated on it without bounds checks.
Impact: ~$5M stolen. Most funds returned after negotiation; attacker retained a bounty fee.
References:
- Coinspect: 1inch Calldata Corruption
- Decurity: Yul Calldata Corruption — 1inch Postmortem
- Halborn: The 1inch Hack (March 2025)
Exploit 2: wagmi-leverage Arbitrary Memory Write via CALLDATACOPY (February 2024)
Root cause: Unchecked multiplication overflow in memory write offset calculation following a CALLDATACOPY operation, allowing an attacker to overwrite whitelisted function selectors.
Details: The wagmi-leverage project’s ExternalCall library used CALLDATACOPY in its _patchAmountAndCall function to copy calldata into a memory buffer. After the copy, the function patched specific values at computed offsets using mstore. The write offset was calculated as add(add(ptr, 0x24), mul(swapAmountInDataIndex, 0x20)), where swapAmountInDataIndex was user-controlled. Because Yul arithmetic is unchecked, a sufficiently large swapAmountInDataIndex caused mul(swapAmountInDataIndex, 0x20) to overflow, wrapping the write address to a small offset that pointed to the function selector region of the buffer. This allowed the attacker to overwrite the whitelisted function selector with an arbitrary value, bypassing the call whitelist and invoking any function on the target contract.
CALLDATACOPY’s role: CALLDATACOPY loaded the attacker’s payload into memory. The subsequent unchecked arithmetic on a user-supplied index (read from the copied calldata) enabled the selector overwrite. The pattern of “CALLDATACOPY + unchecked index arithmetic + mstore” is the common thread between this exploit and the 1inch attack.
Impact: Critical severity finding caught during audit before deployment. Would have allowed arbitrary function calls on whitelisted target contracts.
References:
Exploit 3: Go-ethereum CALLDATACOPY Zero-Padding Bug (2016)
Root cause: Geth’s CALLDATACOPY implementation did not write zeros to memory when the source offset exceeded calldata size, violating the Yellow Paper specification.
Details: Ethereum issue #496 reported that when CALLDATACOPY was called with an offset beyond the end of calldata, geth left the destination memory unchanged (containing stale data from previous operations) instead of zero-filling it as required by the specification. This meant that different EVM implementations could produce different execution results for the same transaction — a consensus-critical divergence.
CALLDATACOPY’s role: The bug was directly in CALLDATACOPY’s implementation. The specification requires that any byte read beyond calldatasize() returns 0x00, but geth’s implementation short-circuited the copy when the offset was out of range, skipping the zero-write entirely.
Impact: Consensus divergence risk between EVM clients. Fixed in subsequent geth releases. The bug underscores that CALLDATACOPY’s zero-padding is a security invariant, not an optimization hint.
References:
Attack Scenarios
Scenario A: Gas Griefing a Meta-Transaction Relayer
contract VulnerableRelayer {
function relay(address target, bytes calldata data) external {
// Relayer pays gas. Copies user's calldata into memory for forwarding.
// No cap on data.length -- attacker sends megabytes of calldata.
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 68, calldatasize()) // 68 = skip selector + target addr
// Memory expansion for multi-MB calldata costs millions of gas
// The actual delegatecall may have only a fraction of gas remaining
let ok := call(gas(), target, 0, ptr, sub(calldatasize(), 68), 0, 0)
}
}
}
// Attack: send 1 MB of junk calldata. Memory expansion alone costs ~24M gas.
// Relayer pays the gas; attacker's on-chain cost is minimal.Attack: Attacker repeatedly calls relay() with enormous calldata. The relayer contract expands memory quadratically, consuming gas the relayer paid for. The actual forwarded call receives too little remaining gas to execute. Relayer is drained of ETH through gas costs while no useful work is done.
Scenario B: Memory Corruption via Overflowed Write Offset
contract VulnerableProcessor {
function processOrder(bytes calldata orderData) external {
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 4, calldatasize())
// Read user-supplied index from calldata
let fieldIndex := mload(add(ptr, 0x40))
// Bug: unchecked mul can overflow, wrapping dst to point
// at the free memory pointer (0x40) or scratch space
let dst := add(ptr, mul(fieldIndex, 0x20))
mstore(dst, mload(add(ptr, 0x60))) // arbitrary memory write
}
}
}
// Attack: set fieldIndex such that mul(fieldIndex, 0x20) overflows to
// (0x40 - ptr), causing mstore to overwrite the free memory pointer.
// All subsequent Solidity memory allocation is corrupted.Scenario C: Zero-Padding Exploit in Proxy Forwarding
contract VulnerableProxy {
address immutable implementation;
constructor(address impl) { implementation = impl; }
fallback() external payable {
assembly {
// Copy calldata + extra 32 bytes (to "ensure alignment")
// Bug: extra bytes beyond calldatasize() are zero-padded
calldatacopy(0, 0, add(calldatasize(), 0x20))
let ok := delegatecall(gas(), sload(implementation.slot), 0, add(calldatasize(), 0x20), 0, 0)
returndatacopy(0, 0, returndatasize())
if ok { return(0, returndatasize()) }
revert(0, returndatasize())
}
}
}
// The implementation receives 32 extra zero bytes appended to every call.
// If the implementation ABI-decodes a dynamic type (bytes, string) and the
// length field points into the zero-padded region, it processes phantom data.
// For a function like transfer(address, uint256), the extra zeros are harmless.
// For a function like multicall(bytes[]), the extra zeros corrupt array decoding.Scenario D: Calldata Truncation in Cross-Contract Call
contract VulnerableBridge {
function deposit(address token, uint256 amount, bytes calldata extraData) external {
assembly {
let ptr := mload(0x40)
// Bug: only copies first 128 bytes of extraData, regardless of actual length
calldatacopy(ptr, extraData.offset, 128)
// If extraData contains a recipient address at byte 130+,
// it's replaced by 0x00...00 (zero-padding)
// Funds are sent to address(0) on the destination chain
}
_bridgeDeposit(token, amount, ptr, 128);
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Memory expansion griefing | Cap calldata length before copying | require(msg.data.length <= MAX_CALLDATA, "too large") |
| T1: Gas griefing in relayers | Estimate and cap gas for sub-calls; charge users proportionally | Use gasleft() checks; bill callers for calldata length |
| T2: Incorrect offset/length | Validate all offset and length values against calldatasize() | if gt(add(ost, len), calldatasize()) { revert(0,0) } in assembly |
| T3: Zero-padding masking | Always check calldatasize() before copying | require(offset + length <= msg.data.length) |
| T3: Phantom zero arguments | Use Solidity’s ABI decoder (which validates calldata bounds) instead of raw assembly | Avoid manual calldatacopy + mload for argument parsing |
| T4: Memory overlap corruption | Use the free memory pointer; update it after writes | let ptr := mload(0x40); mstore(0x40, add(ptr, size)) |
| T5: Unchecked arithmetic on offsets | Validate user-supplied indices before arithmetic | if iszero(lt(index, maxIndex)) { revert(0,0) } before mul(index, 0x20) |
| T5: Overflow in write position | Use SafeMath-equivalent checks in assembly | if lt(mul(a, b), a) { revert(0,0) } for overflow detection |
| General: Assembly complexity | Minimize inline assembly; prefer Solidity’s built-in ABI encoding | Use abi.decode(), msg.data, and high-level calls where possible |
| General: Proxy forwarding | Use audited proxy patterns (OpenZeppelin, EIP-1167) | Standard minimal proxy: calldatacopy(0, 0, calldatasize()) with exact size |
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | High | Gas griefing in relayers and meta-tx executors |
| T2 | Smart Contract | Critical | Medium | 1inch Fusion v1 ($5M), wagmi-leverage |
| T3 | Smart Contract | High | Medium | Zero-padding bugs in proxy forwarding |
| T4 | Smart Contract | Medium | Medium | Memory corruption in complex assembly |
| T5 | Smart Contract | Critical | Medium | 1inch Fusion v1, wagmi-leverage |
| P1 | Protocol | Medium | Low | Block-filling via large calldata |
| P2 | Protocol | Low | Low | Go-ethereum issue #496 (fixed) |
| P3 | Protocol | Medium | Low | Scroll zkEVM gas accounting edge cases |
| P4 | Protocol | Low | Low | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| CALLDATALOAD (0x35) | Loads a single 32-byte word from calldata; same zero-padding behavior for OOB reads but no memory write or expansion cost |
| CALLDATASIZE (0x36) | Returns calldata length; essential for bounds-checking before CALLDATACOPY. Always check calldatasize() before copying |
| CODECOPY (0x39) | Copies bytecode to memory with identical gas formula and zero-padding semantics; same memory expansion griefing vector |
| RETURNDATACOPY (0x3E) | Copies return data to memory; unlike CALLDATACOPY, reverts (not zero-pads) on OOB reads, making it safer but harder to use |
| MCOPY (0x5E) | Memory-to-memory copy (EIP-5656); handles overlapping regions correctly unlike manual CALLDATACOPY + MSTORE patterns |