Opcode Summary

PropertyValue
Opcode0x59
MnemonicMSIZE
Gas2
Stack Input(none)
Stack Outputlen(mem) (size of active memory in bytes, always a multiple of 32)
BehaviorPushes the size of active memory in bytes onto the stack. Active memory is defined by the highest byte offset that has been accessed (read or written) during the current execution context. The returned value is always rounded up to the nearest multiple of 32 (word-aligned). MSIZE starts at 0 and grows monotonically — it never shrinks. Accessing memory at offset o sets MSIZE to at least ceil32(o + 1) if that exceeds the current MSIZE. MSIZE itself does not expand memory; it only reports the current high-water mark. Memory expansion is charged quadratically per the gas formula memory_cost = (words² / 512) + (3 * words).

Threat Surface

MSIZE reflects the highest memory offset touched during execution. It is a read-only observation of memory expansion state, yet this seemingly innocuous property creates a uniquely problematic opcode at the intersection of compiler design, gas economics, and smart contract security.

The threat surface centers on four properties:

  1. MSIZE is a global side-channel for execution state. Every memory access anywhere in the execution context — by any opcode, including MLOAD, MSTORE, MSTORE8, CALLDATACOPY, CODECOPY, EXTCODECOPY, RETURNDATACOPY, CALL (for return data), CREATE, KECCAK256, LOG, and MCOPY — silently advances the MSIZE high-water mark. Code that reads MSIZE is observing the cumulative memory footprint of all prior operations, including operations that have nothing to do with the code reading MSIZE. This makes MSIZE-dependent logic inherently fragile: any change to seemingly unrelated code that touches memory can alter the MSIZE value and break MSIZE-dependent invariants.

  2. MSIZE prevents compiler memory optimizations. The Solidity Yul optimizer treats MSIZE as a side-effectful operation because its return value depends on the memory high-water mark, which can be altered by any memory-touching instruction. If MSIZE appears anywhere in a Yul program, the optimizer cannot freely reorder, eliminate, or merge memory operations because doing so might change the MSIZE value observable to subsequent code. This effectively poisons the entire optimization pipeline for any contract or inline assembly block that uses MSIZE. The Solidity team has documented this as a known limitation (GitHub issues #7125, #7472), and it is a primary reason Solidity’s free memory pointer (mload(0x40)) exists as an alternative to MSIZE-based allocation.

  3. MSIZE was historically used as a memory allocator, creating fragile contracts. Before Solidity standardized the free memory pointer at slot 0x40, some early contracts and Vyper (pre-0.3.x) used MSIZE as a bump allocator — calling MSIZE to find the next free memory offset, then writing there. This pattern is fundamentally broken because any intermediate memory operation (a KECCAK256, a LOG, an ABI encoding step) advances MSIZE, potentially causing the allocator to skip memory regions or collide with data written by other operations. Vyper transitioned away from MSIZE-based allocation toward a structured free memory pointer model after multiple memory-related CVEs.

  4. MSIZE leaks information about execution path and gas consumption. Because MSIZE reflects which code paths have executed (each path may touch different memory offsets), a contract that exposes its MSIZE value (e.g., via a return value or event) reveals information about its internal execution state. In adversarial contexts (e.g., MEV), this allows observers to infer which branches were taken, how much data was processed, or which external calls were made, without needing to trace the full execution.


Smart Contract Threats

T1: MSIZE-Based Memory Allocation Is Fragile and Exploitable (High)

Using MSIZE as a memory allocator — a pattern where msize() provides the “next free” offset — is unreliable because any memory-touching operation between allocation calls silently advances the high-water mark:

  • Allocation gaps and wasted gas. If code between two msize() calls performs a KECCAK256, CALLDATACOPY, or LOG that touches higher memory offsets, the second msize() call returns a value far beyond the expected next-free pointer. The allocator “skips” memory, wasting gas on expanded memory that is never used. Since memory gas cost is quadratic (words² / 512 + 3 * words), unexpected MSIZE jumps translate directly to unexpected gas costs.

  • Data collision when MSIZE is stale. Conversely, if a developer caches an msize() value and writes to it later, other code may have written to that same region in the interim (because MSIZE only reports the high-water mark, not which bytes are free). Two independent allocations based on the same MSIZE value write to the same memory, corrupting each other.

  • Compiler-generated code changes MSIZE unpredictably. Solidity and Vyper generate implicit memory operations for ABI encoding, struct packing, and event emission. A developer using MSIZE in inline assembly cannot predict when these implicit operations advance MSIZE. A compiler version upgrade that changes the order or number of implicit memory operations silently breaks MSIZE-dependent assembly.

Why it matters: MSIZE-based allocation was common in early Solidity (pre-0.4.x) and Vyper (pre-0.3.x) contracts. Legacy contracts still on-chain that use this pattern are susceptible to unexpected gas spikes or data corruption when called by modern contracts that expand memory before the legacy code reads MSIZE.

T2: Unrelated Memory Operations Alter MSIZE-Dependent Logic (High)

Any code that branches or computes based on MSIZE is implicitly dependent on every prior memory operation in the execution context:

  • Cross-function interference. A function that reads MSIZE to determine behavior will produce different results depending on which functions were called before it. Adding a new function to a contract, or changing an existing function’s memory usage, can alter the MSIZE seen by unrelated code.

  • Compiler-version sensitivity. Different Solidity or Vyper versions may generate different memory layouts for the same source code. Code that worked correctly with one compiler version may break with another because the compiler’s generated code touches memory at different offsets, changing MSIZE.

  • External call return data expands memory. When a CALL returns data, the EVM writes the return data to the memory region specified by the caller. If the caller specifies a return data offset beyond the current MSIZE, memory expands. MSIZE-dependent code that runs after an external call sees a different value depending on the return data size and offset of the preceding call.

Why it matters: MSIZE creates invisible coupling between independent code sections. Auditing MSIZE-dependent logic requires analyzing every memory operation in the entire execution path, not just the local function.

T3: Information Leakage via Memory Usage Patterns (Medium)

MSIZE reveals how much memory has been touched, which correlates with execution path and data volume:

  • Execution path fingerprinting. Different code paths (e.g., success vs. error, large vs. small input) touch different memory offsets. If a contract’s MSIZE is observable (via return value, event emission, or gas consumption), an observer can distinguish which path was taken without reading the full execution trace.

  • MEV implications. In the MEV context, searchers simulating transactions can observe MSIZE at various points to infer internal state. For instance, a DEX aggregator that processes different-sized swap paths touches different memory amounts. MSIZE observation during simulation reveals which path the router selected, enabling the searcher to front-run or sandwich with path-specific precision.

  • Gas consumption as a side-channel. Memory expansion is priced quadratically. Higher MSIZE means more gas consumed. Even without directly observing MSIZE, an attacker can infer memory usage from total gas consumption (which is visible on-chain in the transaction receipt). Contracts with MSIZE-dependent branching leak their branch decision through gas usage.

Why it matters: While not a direct exploit vector, information leakage via MSIZE contributes to the broader problem of on-chain execution transparency that MEV actors exploit.

T4: MSIZE Prevents Yul Optimizer from Reducing Gas Costs (Medium)

The Solidity Yul optimizer cannot optimize memory operations in the presence of MSIZE, because optimizations that reorder or eliminate memory accesses would change the observable MSIZE value:

  • Dead store elimination is blocked. If a value is written to memory and never read, the optimizer normally eliminates the write. But if MSIZE is used anywhere, the write cannot be eliminated because removing it would change the MSIZE value.

  • Memory operation reordering is blocked. The optimizer normally reorders memory operations for efficiency (e.g., batch writes to adjacent offsets). With MSIZE present, reordering could change the MSIZE value between two reads, altering program behavior.

  • Loop optimization is degraded. The Yul optimizer transforms loop conditions for efficiency. When MSIZE appears in a loop condition (e.g., for {} msize() {}), the optimizer’s transformation increases code size and gas cost rather than reducing it (Solidity GitHub issue #7472). The optimizer converts for {} msize() {} into for {} 1 {} { if iszero(msize()) { break } }, adding instructions.

  • Inline assembly MSIZE poisons entire functions. A single msize() call in an inline assembly block prevents the optimizer from optimizing any memory operation in the same function, and potentially in calling functions if the compiler cannot prove the MSIZE value is unused.

Why it matters: Contracts using MSIZE in inline assembly pay a hidden tax in the form of larger bytecode and higher gas costs due to missed optimizations. Developers may not realize that a single msize() reference degrades the optimizer’s effectiveness across the entire compilation unit.

T5: Memory Expansion Gas Griefing via MSIZE Interaction (Medium)

While MSIZE itself does not expand memory, the operations that affect MSIZE carry quadratic gas costs, and an attacker can force expensive memory expansion through crafted inputs:

  • Calldata-driven memory expansion. Operations like CALLDATACOPY copy calldata to a memory offset specified on the stack. If a contract copies user-supplied calldata to a memory offset derived from the calldata length (or from MSIZE), an attacker can supply large calldata to force memory expansion to very high offsets. The gas cost grows quadratically: 1 KB of memory costs ~6 gas, but 1 MB costs ~3.2M gas. If the caller does not cap the copy size or destination offset, the attacker forces the contract to consume excessive gas.

  • Return data amplification. A malicious external contract can return a large amount of data. If the calling contract specifies a large return buffer in its CALL instruction, memory expands to accommodate the return data, advancing MSIZE. Subsequent MSIZE-dependent logic sees a much larger value than expected.

  • Quadratic cost escalation. Memory cost is words² / 512 + 3 * words. At 1024 words (32 KB), the cost is ~5,120 gas. At 32,768 words (1 MB), the cost is ~2.1M gas. At 131,072 words (4 MB), the cost exceeds 33M gas — close to the block gas limit. Contracts that use MSIZE to track or limit memory usage must account for this quadratic growth.

Why it matters: Gas griefing via memory expansion is a known EVM attack pattern. MSIZE makes it worse by creating a global observable that encourages contracts to build logic around memory size, increasing exposure to quadratic cost attacks.


Protocol-Level Threats

P1: MSIZE as an Optimizer Barrier in Compiler Infrastructure (Medium)

MSIZE is the primary reason compilers cannot freely optimize memory access patterns:

Solidity’s Yul optimizer limitations: The optimizer treats MSIZE as having side effects equivalent to a memory read at every offset. This means:

  • The “Redundant Store Eliminator” cannot remove stores that only exist to advance MSIZE.
  • The “Load Resolver” cannot substitute memory reads with cached values if MSIZE is read between the store and load.
  • The “Expression Simplifier” cannot fold MSIZE into a constant because its value depends on runtime memory access patterns.

Vyper’s transition away from MSIZE-based allocation: Vyper versions prior to 0.3.x used MSIZE internally for memory management. This design contributed to multiple CVEs (CVE-2024-24561, CVE-2024-26149, CVE-2023-31146) where memory bounds checks failed because the compiler’s own MSIZE-based tracking was inconsistent with actual memory layout. Vyper 0.4.0+ uses a structured free memory pointer model similar to Solidity’s mload(0x40) pattern, eliminating MSIZE from the compiler’s internal memory management.

EOF (EVM Object Format) considerations: Early EOF proposals (EIP-7692 family) considered banning MSIZE in EOF-formatted contracts to unlock optimizer improvements. The “Option D” revision (April 2025) removed the gas-introspection ban, keeping GAS and CALL available, but MSIZE remains a subject of debate. If future EOF iterations ban MSIZE, legacy contracts using it would need migration to EOF-compatible alternatives.

P2: Consensus Safety — MSIZE Is Deterministic (Low)

MSIZE is deterministic: given the same execution trace, every EVM implementation must return the same MSIZE value at every program point. The value depends only on the sequence of memory-accessing instructions executed, and the EVM specification defines exactly how each instruction affects memory size. No consensus bugs have been attributed to MSIZE.

The only subtle implementation detail is the rounding behavior: MSIZE always returns ceil32(highest_byte_offset + 1), where ceil32(x) = ((x + 31) / 32) * 32. All major clients (geth, Nethermind, Besu, Erigon, Reth) agree on this calculation.


Edge Cases

Edge CaseBehaviorSecurity Implication
MSIZE before any memory accessReturns 0Contracts using MSIZE as an allocator before any memory operation get offset 0, which collides with Solidity’s free memory pointer slot (0x00-0x3F are scratch space, 0x40-0x5F is the free memory pointer, 0x60-0x7F is the zero slot).
MSIZE after MSTORE(0, value)Returns 32 (one 32-byte word accessed: offsets 0-31)Accessing offset 0 expands memory to a full 32-byte word.
MSIZE after MSTORE8(0, value)Returns 32 (rounded up from 1 byte at offset 0)Even a single byte access at offset 0 rounds up to 32. MSIZE is always word-aligned.
Non-word-aligned access (e.g., MLOAD(1))Returns 64 (reads bytes 1-32, highest offset is 32, ceil32(32 + 1) = 64)Non-aligned reads expand memory more than expected. MLOAD(1) touches offset 32, expanding to 2 words.
MSIZE after CALLDATACOPY(dest, offset, size)Returns max(current_msize, ceil32(dest + size))CALLDATACOPY with a large dest + size value can advance MSIZE far beyond what the contract’s own logic requires, affecting all subsequent MSIZE-dependent code.
MSIZE after CALL with return data bufferReturns max(current_msize, ceil32(retOffset + retSize))Even if the callee returns no data, the memory region [retOffset, retOffset + retSize) is considered accessed. MSIZE expands to cover the return buffer.
MSIZE after KECCAK256 over memory rangeReturns max(current_msize, ceil32(offset + size))Hashing a memory region expands MSIZE. Contracts using MSIZE after a KECCAK256 see the hash’s memory footprint.
MSIZE never decreasesMonotonically increasing within an execution contextOnce memory is expanded, MSIZE cannot be reduced. There is no way to “free” memory in the EVM. Memory expansion is permanent within a call frame.
MSIZE in a sub-call (CALL/DELEGATECALL)Each call frame has its own memory and MSIZEA sub-call’s memory expansion does not affect the parent’s MSIZE. Memory is per-call-frame.
MSIZE after MCOPY(dest, src, size)Returns max(current_msize, ceil32(max(dest, src) + size))MCOPY (EIP-5656, Cancun) touches both source and destination ranges, expanding MSIZE to cover whichever is higher.

Real-World Exploits

Exploit 1: Vyper Memory Safety CVEs — MSIZE-Adjacent Memory Management Failures (2023-2024)

Root cause: Vyper’s compiler used MSIZE-adjacent memory management patterns that failed to properly track memory bounds, leading to multiple critical vulnerabilities exploited on mainnet.

Details: Vyper’s memory management historically relied on patterns close to MSIZE-based allocation, where the compiler tracked the high-water mark of memory usage to determine where to place new allocations. This approach proved fragile across multiple vulnerability classes:

  • CVE-2024-24561 (CVSS 9.8): slice() bounds check overflow. The Vyper slice() builtin’s runtime bounds check assert(le(add(start, length), src_len)) failed to account for integer overflow when start + length exceeded 2^256. Attackers could craft inputs where the overflowed sum passed the bounds check, enabling out-of-bounds memory reads and writes. This allowed corruption of array length slots, forced return of unrelated memory values, and storage overwrites. The vulnerability existed in all Vyper versions prior to 0.4.0.

  • CVE-2024-26149: _abi_decode memory overflow. Excessively large array start indices in _abi_decode caused the read position to overflow, decoding values from memory regions outside intended bounds. This was exploitable when memory was written between consecutive abi_decode invocations — precisely the scenario where MSIZE-based tracking breaks down.

  • CVE-2023-31146 (CVSS 9.1): DynArray out-of-bounds access. The compiler wrote the length word of dynamic arrays before data during codegen. When a DynArray appeared on both sides of an assignment, this ordering caused out-of-bounds array access and cross-call-frame data corruption.

MSIZE’s role: These vulnerabilities share a common root: the compiler’s memory management relied on tracking the high-water mark of memory usage (the same state MSIZE reports) rather than maintaining a structured free memory pointer with explicit bounds checking. After the 2023 Curve Finance exploit (which drained $70M+ from Vyper-compiled pools due to a broken reentrancy guard), the Vyper team undertook a comprehensive security overhaul including 12 audits and a transition to a structured free memory pointer model (VIP #4426) that eliminated MSIZE-based memory tracking from the compiler internals.

Impact: Cumulative impact across Vyper CVEs: hundreds of millions of dollars at risk. The Curve exploit alone drained $70M+.

References:


Exploit 2: 1inch Fusion v1 — Memory Layout Manipulation Leading to $5M Drain (March 2025)

Root cause: Low-level assembly code in 1inch’s Fusion v1 contract mishandled memory offsets during calldata processing, allowing an attacker to corrupt memory layout and forge resolver addresses.

Details: The 1inch Fusion v1 contract’s _settleOrder() function used inline assembly for performance-critical calldata parsing. The attacker provided a malicious interactionLength value that caused an integer underflow when calculating memory write positions. This underflow placed data at unexpected memory offsets, effectively overwriting the resolver address used for settlement authorization. The attacker impersonated a legitimate resolver and extracted funds.

MSIZE’s role: The exploit demonstrates the fundamental risk of building logic around EVM memory layout. The vulnerable contract’s assembly code assumed a specific memory layout based on prior operations. The attacker subverted this assumption by manipulating input lengths that shifted where data landed in memory. While the contract did not use MSIZE directly, the attack class — memory layout manipulation via crafted inputs affecting memory offsets — is precisely what makes MSIZE-dependent allocation dangerous. Any contract that derives “where to write next” from memory state (whether via MSIZE or manual offset tracking) is vulnerable to this class of attack when offset calculations involve user-controlled values.

Impact: ~$5M stolen from 1inch Fusion v1.

References:


Exploit 3: Solidity Optimizer Misbehavior with MSIZE — Gas Cost Inflation (2019-Present, Ongoing)

Root cause: The Solidity Yul optimizer generates worse code (larger bytecode, higher gas costs) when MSIZE is present, contrary to its purpose of optimizing code.

Details: Documented in Solidity GitHub issues #7472 and #7125, the Yul optimizer mishandles code containing MSIZE in loop conditions and expressions. Specifically:

  • When MSIZE appears as a loop condition (for {} msize() {}), the optimizer transforms it into a longer, more expensive equivalent: for {} 1 {} { if iszero(msize()) { break } }. This increases both bytecode size and gas cost per iteration.

  • The optimizer’s “Unused Pruner” step cannot remove unused memory stores when MSIZE is present, because removing them would change the observable MSIZE value. Test case unusedPrunerWithMSize.yul demonstrated that optimized and unoptimized versions produced identical execution traces — the optimizer provided zero benefit.

  • The free memory pointer overflow check differs between the legacy codegen and Sol→Yul codegen (Solidity issue #11709). Legacy codegen does not check for free memory pointer overflow beyond UINT64_MAX, while Sol→Yul emits a panic(0x41). Contracts using MSIZE instead of the free memory pointer bypass both checks entirely, risking silent memory corruption on extreme inputs.

MSIZE’s role: MSIZE is the direct cause of the optimizer’s inability to improve code. The optimizer must conservatively assume that any memory operation’s effect on MSIZE is observable, preventing dead store elimination, operation reordering, and constant folding across memory-touching code.

Impact: No direct financial exploit, but all Solidity contracts using MSIZE in inline assembly pay unnecessary gas costs and deploy larger bytecode. This constitutes a systemic inefficiency affecting gas costs for users of affected contracts.

References:


Attack Scenarios

Scenario A: MSIZE-Based Allocator Collision

contract VulnerableMSIZEAllocator {
    // Uses MSIZE as a bump allocator in inline assembly
    function processData(bytes calldata data) external pure returns (bytes32) {
        bytes32 result;
        assembly {
            // Allocate memory at MSIZE for temporary buffer
            let bufferPtr := msize()
            calldatacopy(bufferPtr, data.offset, data.length)
 
            // VULNERABLE: This keccak256 expands memory beyond bufferPtr + data.length
            // because it reads from the buffer region, but now MSIZE has advanced
            result := keccak256(bufferPtr, data.length)
 
            // Second allocation: MSIZE has jumped past the keccak256's memory footprint.
            // This wastes memory and gas. In a more complex scenario,
            // another component might have written to the region between
            // the old and new MSIZE values.
            let secondPtr := msize()
            // secondPtr is NOT bufferPtr + data.length -- it's much higher
            // due to keccak256's internal memory expansion
            mstore(secondPtr, result)
        }
        return result;
    }
}
 
// Attack: caller provides large calldata (e.g., 32 KB).
// calldatacopy expands memory to 32 KB → MSIZE = 32768.
// keccak256 over 32 KB region doesn't further expand (already covered).
// But if the contract had intermediate operations (ABI encoding, event emission)
// that touched higher offsets, secondPtr would be unpredictably high,
// causing quadratic gas cost escalation.

Scenario B: Compiler Upgrade Breaks MSIZE-Dependent Logic

contract MSIZEVersionSensitive {
    event Processed(uint256 indexed id, uint256 memUsed);
 
    function process(uint256 id, uint256[] calldata values) external {
        uint256 memBefore;
        uint256 memAfter;
 
        assembly { memBefore := msize() }
 
        // This high-level Solidity code generates implicit memory operations
        // for ABI encoding, array bounds checks, and event emission.
        // The exact memory layout depends on the Solidity compiler version.
        uint256 sum;
        for (uint256 i = 0; i < values.length; i++) {
            sum += values[i];
        }
 
        assembly { memAfter := msize() }
 
        // VULNERABLE: memAfter - memBefore varies across compiler versions.
        // Solidity 0.8.20 might use 3 words of scratch space; 0.8.26 might use 5.
        // Any logic based on this delta is version-dependent and fragile.
        emit Processed(id, memAfter - memBefore);
 
        // Worse: using the delta for allocation or validation
        require(memAfter - memBefore < 1024, "excessive memory");
        // A compiler upgrade could trip this require with identical source code.
    }
}

Scenario C: Memory Expansion Gas Griefing via Return Buffer

interface IOracle {
    function getPrice(address token) external view returns (uint256);
}
 
contract VulnerableConsumer {
    IOracle public oracle;
 
    function swap(address tokenIn, address tokenOut, uint256 amount) external {
        // VULNERABLE: staticcall with a large return buffer expands memory
        // even if the callee returns only 32 bytes
        uint256 price;
        assembly {
            // Allocate 4096 bytes for return data "just in case"
            let retPtr := msize()
            let success := staticcall(
                gas(),
                sload(oracle.slot),
                0, 0,         // no input
                retPtr, 4096  // 4 KB return buffer
            )
            // Memory is now expanded by 4096 bytes regardless of actual return size.
            // MSIZE jumped by 4096, and quadratic gas cost increased.
            price := mload(retPtr)
        }
 
        // If oracle is a malicious contract (or the oracle address is attacker-controlled),
        // the attacker can set retPtr to an extreme offset by first expanding memory
        // with other operations. The 4096-byte return buffer then sits at a high offset,
        // triggering massive quadratic gas costs.
 
        _executeSwap(tokenIn, tokenOut, amount, price);
    }
 
    function _executeSwap(address, address, uint256, uint256) internal { /* ... */ }
}

Scenario D: MSIZE Information Leakage in MEV Context

contract DEXRouter {
    function route(
        address[] calldata path,
        uint256 amountIn
    ) external returns (uint256 amountOut) {
        // Each swap in the path uses different memory amounts
        // depending on the pool type (Uniswap V2 vs V3 vs Curve)
        for (uint256 i = 0; i < path.length - 1; i++) {
            amountIn = _swap(path[i], path[i + 1], amountIn);
        }
        amountOut = amountIn;
    }
 
    function _swap(address tokenIn, address tokenOut, uint256 amount)
        internal returns (uint256)
    {
        // V2 pools use ~256 bytes of memory (simple getReserves + swap)
        // V3 pools use ~2048 bytes (tick bitmap, sqrt price computation)
        // Curve pools use ~4096 bytes (Newton's method iterations)
 
        // INFORMATION LEAK: Total gas consumption (visible in tx receipt)
        // correlates with memory expansion (quadratic cost).
        // A MEV searcher simulating this transaction can observe the
        // gas used to infer which pool types are in the path,
        // then craft a more precise sandwich attack.
 
        // ... swap logic ...
        return amount;
    }
}

Mitigations

ThreatMitigationImplementation
T1: MSIZE-based allocationUse Solidity’s free memory pointer instead of MSIZElet ptr := mload(0x40) in inline assembly; update with mstore(0x40, add(ptr, size)) after allocation
T1: Data collision from stale MSIZENever cache MSIZE across operations that touch memoryIf MSIZE must be used, read it immediately before use; never store it in a variable across external calls or complex operations
T2: Cross-function MSIZE interferenceAvoid MSIZE in business logic entirelyReplace all MSIZE-dependent branching with explicit state variables or function parameters
T2: Compiler-version sensitivityPin compiler versions; test MSIZE values across versionsUse solc --optimize --ir to inspect Yul output for MSIZE interactions; add gas-based regression tests
T3: Information leakage via memoryDo not expose MSIZE values externallyNever return or emit MSIZE values; use constant-memory-footprint algorithms where MEV resistance is needed
T4: Optimizer degradationRemove MSIZE from inline assemblyReplace msize() with mload(0x40) (Solidity free memory pointer); the optimizer can then freely optimize memory operations
T5: Memory expansion gas griefingCap memory offsets derived from user inputValidate dest + size before CALLDATACOPY, RETURNDATACOPY; set maximum return buffer sizes in CALL; use returndatasize() to allocate exact-fit return buffers
T5: Quadratic gas escalationSet explicit gas limits for memory-intensive operationsUse staticcall(GAS_LIMIT, ...) with bounded gas; check returndatasize() after calls and only copy what was returned
General: Legacy contract riskAudit inline assembly for MSIZE usageSearch for msize in all assembly blocks; replace with free memory pointer pattern; re-audit after changes

Compiler/EIP-Based Protections

  • Solidity free memory pointer (0x40): Solidity’s standard memory management uses mload(0x40) as a bump allocator. This is immune to the MSIZE fragility problems because it tracks allocation state in a known memory slot rather than relying on the global high-water mark. All Solidity versions since 0.4.x use this pattern for compiler-generated code.

  • Vyper 0.4.0+ structured memory model: Vyper eliminated MSIZE-based memory tracking in its compiler internals, adopting a free memory pointer model with explicit bounds checking. This fixed the root cause of multiple CVEs (CVE-2024-24561, CVE-2024-26149, CVE-2023-31146).

  • EIP-5656 (MCOPY, Cancun 2024): Introduces a dedicated memory-to-memory copy instruction that is more gas-efficient than the MLOAD/MSTORE loop pattern. MCOPY’s memory expansion behavior is well-defined and predictable, reducing the need for MSIZE-based memory management in copy-heavy operations.

  • EOF (EIP-7692 family, Fusaka): The EVM Object Format introduces deploy-time code validation. While the current “Option D” revision retains MSIZE, future EOF iterations may restrict or deprecate MSIZE to unlock compiler optimizations. Contracts targeting EOF should avoid MSIZE to ensure forward compatibility.

  • EIP-1153 (Transient Storage, Cancun 2024): For data that needs to persist within a transaction but not across transactions, transient storage (TLOAD/TSTORE) is a better alternative than memory for cross-function communication, avoiding MSIZE-related coupling.


Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighMediumVyper memory CVEs (CVE-2024-24561, CVE-2023-31146); early Solidity MSIZE allocator bugs
T2Smart ContractHighMedium1inch Fusion v1 memory layout exploit ($5M); compiler-version-dependent memory behavior
T3Smart ContractMediumMediumMEV gas profiling; execution path fingerprinting via gas receipts
T4Smart ContractMediumHighSolidity optimizer issues #7472, #7125 (systemic gas overhead, no financial exploit)
T5Smart ContractMediumMediumMemory expansion gas griefing (generic EVM attack pattern)
P1ProtocolMediumHighVyper compiler overhaul post-Curve exploit; Solidity optimizer limitations (documented, ongoing)
P2ProtocolLowN/ANo consensus bugs; deterministic across all clients

OpcodeRelationship
MLOAD (0x51)Reads 32 bytes from memory at a given offset. Expands memory and advances MSIZE if the read touches bytes beyond the current high-water mark. MLOAD(offset) sets MSIZE to at least ceil32(offset + 32).
MSTORE (0x52)Writes 32 bytes to memory at a given offset. Expands memory and advances MSIZE identically to MLOAD. The primary memory-writing instruction that affects MSIZE.
MSTORE8 (0x53)Writes a single byte to memory. Despite writing only 1 byte, expands MSIZE to ceil32(offset + 1) — a full 32-byte word. This rounding behavior surprises developers expecting byte-granularity.
MCOPY (0x5E)Copies memory from source to destination range (EIP-5656, Cancun). Expands MSIZE to cover both source and destination ranges. More gas-efficient than MLOAD/MSTORE loops for bulk copies, and its memory expansion behavior is predictable.