Opcode Summary

PropertyValue
Opcodes0x80 (DUP1) through 0x8F (DUP16)
MnemonicDUP1, DUP2, … DUP16
Gas3 (all variants)
Stack InputRequires at least n items on the stack for DUPn
Stack OutputClones the nth stack item (1-indexed from the top) and pushes the copy to the top
BehaviorDUPn duplicates the stack item at depth n (where DUP1 = top of stack, DUP16 = 16th item from top) and pushes it onto the stack, increasing the stack size by 1. The original item remains in place. All 16 variants are equivalent in gas cost and execution semantics, differing only in which stack depth they target. Stack underflow (fewer than n items) or stack overflow (stack at 1024 items) causes an exceptional halt that consumes all remaining gas.

Threat Surface

DUP is the EVM’s core value-cloning primitive. Every time a Solidity compiler needs to reuse a local variable, pass the same argument to multiple operations, or preserve a value across a computation, it emits a DUP instruction. The DUP family is among the most frequently executed opcodes in deployed bytecode — and that ubiquity is exactly what makes its failure modes dangerous. DUP itself is mechanically trivial (clone a stack slot), but its security surface emerges from how it interacts with the stack machine model and compiler code generation.

The threat surface centers on four properties:

  1. Stack underflow causes an immediate exceptional halt. If DUPn is executed when the stack contains fewer than n items, the EVM does not return an error or push a default value — it halts execution and consumes all remaining gas in the call frame. In compiler-generated code this is virtually impossible (the compiler tracks stack depth statically), but in handwritten assembly (assembly {} blocks in Solidity, raw Yul, or Huff) there is no compile-time stack depth guarantee. A misplaced DUP in a hand-optimized routine can cause a function to unconditionally revert with zero diagnostic information, since the exceptional halt produces no return data.

  2. Stack overflow at 1024 items is an exceptional halt. Each DUP increases the stack depth by 1. If the stack is already at the 1024-item maximum, any DUP causes an exceptional halt identical to underflow. While reaching 1024 items in normal code is unusual, deeply recursive inline computations, unrolled loops in assembly, or adversarial contract interactions can push the stack toward the limit. The EVM provides no opcode to query current stack depth, so contracts cannot defensively check how close they are to overflow.

  3. Wrong DUP index in assembly silently uses the wrong value. DUP1 vs. DUP2 is a single bit difference in the opcode byte (0x80 vs. 0x81), but the semantic difference is using an entirely different variable. In handwritten assembly, an off-by-one DUP index error does not cause a revert — the EVM happily duplicates whichever stack item the opcode specifies. The wrong value propagates silently through subsequent computation, producing incorrect results that may only manifest under specific inputs or edge-case conditions. This is the assembly-level analog of using the wrong variable name in a high-level language, except there are no named variables to catch the mistake.

  4. Compiler optimization bugs can generate incorrect DUP instructions. High-level compilers (Solidity, Vyper) manage the stack internally and emit DUP/SWAP sequences to shuttle values to where they’re needed. Optimizer passes that reorder, deduplicate, or eliminate code can miscalculate stack depths, causing the wrong DUP index to be emitted in the final bytecode. These bugs are rare but catastrophic when they occur: the generated code silently operates on wrong values, and the source code gives no indication of the problem. The Solidity compiler’s StackLayoutGenerator and Vyper’s double-evaluation bugs are examples of this class.


Smart Contract Threats

T1: Wrong DUP Index in Handwritten Assembly (High)

Inline assembly in Solidity and raw Yul/Huff code require the developer to mentally track the stack layout at every instruction. A DUP with the wrong index reads a completely different value, and the EVM provides zero indication that the wrong slot was accessed:

  • Off-by-one errors. The most common mistake. After a PUSH or CALL instruction changes the stack layout, all subsequent DUP indices must be adjusted. If a developer adds an instruction that pushes a value mid-routine but forgets to increment DUP indices for later references, every subsequent DUP targets the wrong slot. DUP2 instead of DUP1 means using a stale value instead of a freshly computed one, or vice versa.

  • Silent value substitution. Unlike a variable name typo in Solidity (which triggers a compile error), a wrong DUP index compiles and deploys without warning. The bytecode is syntactically valid. The contract may appear to work correctly in common cases if the two stack positions happen to hold similar or equivalent values during testing, only to fail under adversarial or edge-case inputs.

  • Cascading errors. A single wrong DUP can corrupt an entire computation chain. If a DUP error causes an incorrect memory pointer to be used in MSTORE, subsequent MLOAD operations read garbage. If it substitutes a wrong address in a CALL, funds are sent to the wrong recipient. The error surface fans out exponentially from the initial wrong DUP.

Why it matters: Gas-optimized DeFi contracts, MEV searcher bots, and precompile wrappers frequently use inline assembly for performance. Every DUP in these hand-optimized routines is a potential silent value substitution bug.

T2: Stack Underflow — Exceptional Halt with No Diagnostics (Medium)

DUPn requires at least n items on the stack. If the precondition is violated, the EVM triggers an exceptional halt:

  • All remaining gas is consumed. Unlike a REVERT (which refunds unused gas), an exceptional halt from stack underflow burns the entire gas allocation of the current call frame. In a high-gas-limit transaction, this can waste millions of gas units.

  • No return data. The caller receives a failure indication (the CALL returns 0) but no revert reason, no error selector, and no diagnostic data. Debugging requires transaction tracing at the opcode level.

  • Assembly-only risk. The Solidity compiler statically verifies stack depth during compilation, making underflow impossible in compiler-generated code. The risk is confined to handwritten assembly {} blocks, raw Yul, Huff, and bytecode generated by custom tooling. A conditional code path in assembly that skips a PUSH but not the corresponding DUP will underflow on the branch where the PUSH was skipped.

Why it matters: Handwritten assembly is common in gas-sensitive protocols (DEX routers, bridge contracts, precompile wrappers). An underflow in a rarely-taken branch can lurk undetected through testing and auditing, surfacing only when an attacker crafts specific inputs.

T3: Stack Overflow at 1024-Item Limit (Low)

Each DUP increases the stack depth by 1. The EVM enforces a hard limit of 1024 items:

  • Deeply nested computations. Compiler-generated code rarely exceeds a stack depth of ~20, but complex inline assembly routines with unrolled loops, recursive macros (Huff), or repeated DUP-heavy patterns can theoretically approach the limit.

  • No stack depth introspection. There is no opcode to query the current stack depth. A contract cannot defensively check if (stackDepth > 1020) revert(). The only protection is static analysis of the bytecode before deployment.

  • Exceptional halt semantics. Overflow causes the same all-gas-consuming halt as underflow: no return data, no revert reason, caller sees a bare failure.

Why it matters: While practically rare in isolation, stack overflow becomes relevant when combined with adversarial call patterns. A contract that allows external callers to influence the number of iterations in an assembly loop (e.g., processing a user-supplied array in inline assembly with DUP-heavy per-element logic) could be pushed toward the limit.

T4: Compiler Optimization Bugs Generating Incorrect DUP Sequences (High)

The Solidity and Vyper compilers emit DUP instructions as part of their internal stack scheduling. Optimizer passes can introduce bugs that cause incorrect DUP indices to be generated:

  • Solidity’s Block Deduplicator bug (0.8.5-0.8.23). The optimizer incorrectly considered different verbatim assembly blocks as identical and merged them, causing code that should use different DUP/stack sequences to share a single (wrong) code path. While the trigger conditions were narrow (pure Yul, optimizer enabled, multiple verbatim blocks), the impact was arbitrary code execution divergence from source semantics.

  • Solidity’s StackLayoutGenerator assertion failures (pre-0.8.21). The IR-based compiler’s stack layout calculator could fail when Yul return variables were read before assignment, causing incorrect stack depth assumptions that propagated as wrong DUP indices in generated bytecode.

  • Vyper’s double-evaluation vulnerability (CVE-2025-27104). Vyper’s code generator evaluated iterator expressions multiple times in for loops, causing the generated DUP/stack operations to reference stale or side-effect-corrupted values. State-modifying expressions in loop iterators produced silently incorrect behavior.

  • Vyper’s nonreentrant lock misallocation (CVE-2023-39363). While not a DUP bug per se, the compiler’s storage slot allocation for reentrancy guards generated code that checked incorrect slots, functionally equivalent to a wrong-DUP error at the code generation level. This enabled the $70M+ Curve Finance exploit.

Why it matters: Developers cannot inspect the DUP instructions generated by the compiler without disassembling the bytecode. Compiler bugs that produce wrong DUP sequences are invisible at the source level and can affect every contract compiled with the buggy version.

T5: DUP Copies Sensitive Values That Should Be Consumed (Medium)

DUP clones a value without removing the original. If a value should be used exactly once (e.g., a nonce, a one-time authorization token, or a computed hash that gates a state transition), DUP’ing it instead of consuming it leaves a copy on the stack that can be reused:

  • Replay within a single execution. In handwritten assembly, if a one-time value (such as a computed authorization hash) is DUP’d rather than consumed by the operation that uses it, subsequent code may reuse the leftover copy to repeat the authorized action. This is a within-transaction replay at the opcode level.

  • Incorrect nonce handling. A contract that implements its own nonce scheme in assembly might DUP the nonce value to check it and then increment it. If the DUP’d copy is not properly POP’d and instead gets used by a later operation, the nonce check can be bypassed or the increment can be applied to the wrong slot.

  • Secret leakage through stack residue. When a sensitive value (private key material in a custom crypto operation, intermediate ECDSA values in assembly-level signature verification) is DUP’d for computation, copies may linger on the stack longer than necessary. While the EVM clears the call frame’s stack on return, within-frame code that reads deeper stack positions (via DUP or SWAP) could access these residual values.

Why it matters: The distinction between “use” and “copy” is fundamental to correctness. DUP is the only mechanism to access a stack value without consuming it, and its misuse creates a class of bugs where values are unintentionally available for reuse.


Protocol-Level Threats

P1: No DoS Vector from DUP Itself (Low)

All DUP variants cost a fixed 3 gas with no dynamic component. DUP reads from the execution stack (an in-memory data structure), not from persistent storage or external state. It cannot trigger disk I/O, state trie traversal, or any operation with variable cost. A contract that executes millions of DUP instructions pays 3 gas per DUP, which is correctly priced and poses no DoS risk to nodes.

P2: EIP-663 / EIP-8024 — Extended Stack Access (Medium)

Multiple EIPs propose extending DUP semantics beyond the current 16-item limit:

  • EIP-663 (DUPN, SWAPN, EXCHANGE): Introduces new opcodes that accept an immediate operand specifying the stack depth, enabling access to any of the 1024 stack positions. Targets EOF (Ethereum Object Format) bytecode only. The immediate operand is statically analyzable, preserving security audit capabilities.

  • EIP-8024 (backward-compatible DUPN/SWAPN): Achieves the same goal without requiring EOF, using immediate bytes following the instruction. This allows existing tooling to support extended stack access.

  • EIP-7912 (pragmatic DUP17-24/SWAP17-24): Extends the DUP/SWAP families with 8 additional variants (DUP17-24) plus dynamic DUPN/SWAPN preceded by PUSH.

Security implications: Extended stack access eliminates the current workaround of spilling variables to memory when more than 16 are needed, reducing memory-related bugs. However, DUPN with a dynamic operand (EIP-7912) makes static stack analysis harder, since the target depth is only known at runtime. All proposals maintain the 3-gas cost and the stack overflow/underflow halt semantics.

P3: EIP-5450 — EOF Stack Validation (Low)

EIP-5450 proposes deploy-time validation of stack usage in EOF code sections. Under this scheme, the EVM verifies at deployment that no code path can cause stack underflow or overflow, eliminating the need for runtime DUP underflow/overflow checks entirely. This transforms DUP underflow from a runtime exceptional halt into a deployment-time rejection, substantially reducing the attack surface for T2 and T3.

P4: Consensus Safety (Low)

DUP semantics have never changed across Ethereum hard forks. All client implementations (geth, Nethermind, Besu, Erigon, reth) agree on the behavior: clone the nth stack item, push to top, exceptional halt on underflow or overflow. No consensus bugs have been attributed to DUP instructions.


Edge Cases

Edge CaseBehaviorSecurity Implication
DUP1 with exactly 1 item on the stackDuplicates the sole item; stack grows from 1 to 2 itemsMinimum valid stack depth for DUP1. Succeeds normally.
DUP16 with exactly 16 items on the stackDuplicates the bottom item; stack grows from 16 to 17 itemsMinimum valid stack depth for DUP16. The 16th item (deepest accessible) is cloned to the top.
DUP1 with 0 items on the stackExceptional halt; all remaining gas consumedStack underflow. No return data, no revert reason. Caller sees a bare call failure.
DUP16 with 15 items on the stackExceptional halt; all remaining gas consumedStack underflow for DUP16 specifically — 15 items suffice for DUP1-DUP15 but not DUP16.
DUP with stack at 1024 itemsExceptional halt; all remaining gas consumedStack overflow. DUP would push a 1025th item, exceeding the hard limit.
DUP in DELEGATECALL contextOperates on the delegate’s execution stack, not the caller’sDUP reads the local call frame’s stack. DELEGATECALL creates a new stack frame. No cross-frame stack leakage.
DUP in STATICCALL contextIdentical behavior to normal executionSTATICCALL restricts state writes, not stack operations. DUP is unaffected.
DUP of a value that was MSTORE’d then MLOAD’dDUP copies the 32-byte word on the stack, not a memory referenceThe EVM stack holds values, not references. DUP’d values are independent copies. Modifying memory after DUP does not change the DUP’d value.
DUP after CALL that returned 0 (failure)DUP copies whatever value is on top of the stack (the CALL’s return flag: 0)No special behavior. The developer must check the return value before DUP’ing further up the stack; otherwise, subsequent logic operates on stale pre-CALL values.
Sequential DUP16 x64Each DUP16 adds 1 item; 64 consecutive DUP16 instructions add 64 itemsStack grows linearly. Approaches overflow in deep assembly routines but each DUP is independently validated.
DUP of address(0) or other zero-valueDuplicates the 32-byte zero value normallyZero is a valid stack value. DUP does not distinguish between zero and non-zero. No special-case behavior.

Real-World Exploits

Exploit 1: Vyper Compiler Nonreentrant Guard Bug — Curve Finance Pools Drained ($70M+, July 2023)

Root cause: Vyper compiler versions 0.2.15-0.3.0 generated incorrect code for the @nonreentrant decorator, allocating separate storage slots for each function’s reentrancy lock instead of sharing one slot per lock key. The generated bytecode loaded and checked wrong storage slots — a code-generation-level error functionally equivalent to emitting the wrong DUP index for a shared variable.

Details: The @nonreentrant("lock") decorator was supposed to generate code that checked and set a single shared storage slot across all functions using the same lock key. Instead, the compiler allocated a distinct storage slot per function, so the remove_liquidity function’s lock variable was at a different slot than add_liquidity’s lock variable, even when both used @nonreentrant("lock"). The generated bytecode correctly loaded and checked its own slot but was blind to the other function’s lock state.

Attackers exploited this by calling remove_liquidity on Curve pools (alETH-ETH, msETH-ETH, pETH-ETH, CRV-ETH). When the pool transferred ETH to the attacker’s contract, the attacker’s receive() fallback reentered via add_liquidity. The reentrancy guard, checking a different storage slot, did not detect the reentry. The attacker manipulated pool invariants during the inconsistent state to extract excess funds.

DUP family’s role: The root cause was in the compiler’s code generation for storage variable references — the same class of bug as emitting the wrong DUP index. The compiler’s internal stack/variable management produced bytecode that referenced incorrect storage locations, and the mismatch was invisible at the Vyper source level. This demonstrates that compiler bugs in stack/variable management silently produce wrong bytecode even when the source code is correct.

Impact: ~$70M+ drained across multiple Curve pools. Exploits occurred over July 30-31, 2023. The bug affected every Vyper contract compiled with versions 0.2.15-0.3.0 that used @nonreentrant.

References:


Exploit 2: Solidity Verbatim Block Deduplication — Incorrect Code Merging (0.8.5-0.8.23, November 2023)

Root cause: The Solidity compiler’s Block Deduplicator optimizer pass incorrectly treated different verbatim assembly blocks as identical when they were surrounded by the same opcodes. The optimizer merged distinct code paths into a single block, causing the generated bytecode to execute the wrong instruction sequence.

Details: The verbatim feature allows injecting raw bytecode into Yul code. When two verbatim blocks with different payloads were surrounded by identical DUP/PUSH/SWAP sequences, the Block Deduplicator considered them identical and eliminated one, redirecting control flow to the surviving copy. The effect was that code intended to execute different bytecode sequences (potentially different DUP patterns for accessing different stack variables) would execute the same sequence, silently using wrong values.

The bug required narrow trigger conditions (pure Yul compilation, optimizer enabled, multiple verbatim blocks with different data surrounded by identical opcodes), limiting real-world impact. However, the bug class — optimizer merging code paths that should remain distinct because they access different stack positions — is directly relevant to DUP correctness.

DUP family’s role: The deduplicator’s decision to merge blocks was based on matching the DUP/SWAP/PUSH sequences surrounding the verbatim blocks. The optimizer assumed identical surrounding stack operations meant identical semantics, but the verbatim payloads differed. This is a concrete example of optimizer-level DUP-sequence analysis going wrong.

Impact: No confirmed exploits in production. Discovered and patched before known exploitation. Fixed in Solidity 0.8.23.

References:


Exploit 3: Call Depth Attack — Stack Limit Exploitation (Pre-EIP-150, 2016)

Root cause: Before EIP-150 (Tangerine Whistle, October 2016), attackers could artificially inflate the EVM call stack to near its 1024-frame limit, causing external calls in victim contracts to silently fail. While this exploits the call stack rather than the data stack that DUP operates on, it demonstrates the EVM stack-limit attack class and the danger of silent failures at stack boundaries.

Details: An attacker would recursively call their own contract 1023 times, then call the victim contract. When the victim attempted to make an external call (e.g., send() to refund a previous bidder in an auction), the call would fail because the call stack was already at its limit. Critically, send() returned false rather than reverting, and many contracts did not check the return value. The attacker’s bid was recorded, but the previous bidder’s refund silently failed.

The attack exploited the same 1024-item limit that constrains DUP’s data stack. Both the call stack (1024 frames) and the data stack (1024 items per frame) share the same architectural limit, and both cause silent failure when exceeded.

DUP family’s role: Indirect but architecturally related. The 1024-item stack limit is a shared constraint across the EVM’s stack-based architecture. EIP-150 mitigated the call-depth variant by limiting child calls to 63/64 of parent gas (making 1024 depth unreachable in practice), but the data stack overflow from DUP remains unmitigated by any gas rule — it is purely a count-based exceptional halt.

Impact: Multiple auction and gambling contracts were exploited before EIP-150. The Ethereum Foundation’s 2016 security advisory explicitly warned about call depth attacks.

References:


Attack Scenarios

Scenario A: Wrong DUP Index in Assembly — Silent Value Substitution

contract VulnerableRouter {
    function swapExactTokens(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 minAmountOut,
        address recipient
    ) external {
        assembly {
            // Stack after loading parameters:
            // [recipient, minAmountOut, amountIn, tokenOut, tokenIn]
            
            // Developer intends to DUP tokenOut for the swap call,
            // but miscounts the stack depth after intermediate operations
            
            let balanceBefore := balance(address())
            // Stack shifted by 1 due to balanceBefore push
            
            // BUG: Should be DUP6 (tokenOut) but uses DUP5 (amountIn)
            // because developer forgot balanceBefore shifted the stack
            let target := sload(0x00) // DEX pool address
            
            // Calls the pool with amountIn as the "tokenOut" parameter,
            // which is interpreted as an address, sending tokens to a
            // garbage address derived from the uint256 amountIn value.
            // Funds are permanently lost.
            
            // In opcode terms: DUP5 instead of DUP6 silently substitutes
            // the amountIn integer for the tokenOut address.
        }
    }
}

Scenario B: Stack Underflow in Conditional Assembly Path

contract VulnerableVault {
    function withdraw(uint256 amount, bool usePermit) external {
        assembly {
            if usePermit {
                // This branch pushes a permit validation result onto the stack
                let permitValid := call(gas(), 0x01, 0, 0, 0, 0, 0x20)
                // Stack: [permitValid, ...]
            }
            // BUG: DUP1 assumes permitValid is on the stack,
            // but when usePermit == false, the if-branch was skipped
            // and permitValid was never pushed.
            
            // When usePermit == false: DUP1 duplicates whatever
            // happens to be on top of the stack (e.g., 'amount').
            // If stack is empty at this point: exceptional halt,
            // all gas consumed, no revert reason.
            
            // The contract works in testing (usePermit == true)
            // but fails catastrophically when usePermit == false.
        }
    }
}

Scenario C: DUP-Based Stack Overflow in Unrolled Loop

contract VulnerableBatchProcessor {
    function processBatch(bytes calldata data) external {
        assembly {
            // Hand-optimized batch decoder that DUPs intermediate results
            // to avoid re-reading from calldata
            let offset := 0
            let count := calldataload(add(data.offset, offset))
            offset := add(offset, 32)
            
            // Each iteration DUPs 3-4 values for reuse across sub-operations.
            // With a user-controlled 'count' of 300+ and 3 DUPs per iteration,
            // the stack grows by ~900 items on top of existing depth.
            // At count ~340, the stack hits 1024 and the contract halts.
            
            for { let i := 0 } lt(i, count) { i := add(i, 1) } {
                let item := calldataload(add(data.offset, offset))
                // DUP item for validation
                // DUP item for processing
                // DUP item for event emission
                // ... items accumulate on stack if not POP'd
                offset := add(offset, 32)
            }
            
            // An attacker submits data with count = 400.
            // Around iteration 340, DUP triggers stack overflow.
            // All gas consumed, no return data, transaction fails silently.
        }
    }
}

Scenario D: DUP Copies One-Time Value — Within-Transaction Replay

contract VulnerableOneTimeAuth {
    mapping(bytes32 => bool) public usedTokens;
    
    function executeWithToken(bytes32 authToken, address target, bytes calldata payload) external {
        assembly {
            // Load authToken onto stack
            let token := calldataload(4)
            
            // Check if token is used (correct)
            mstore(0x00, token)
            mstore(0x20, usedTokens.slot)
            let slot := keccak256(0x00, 0x40)
            let used := sload(slot)
            if used { revert(0, 0) }
            
            // Mark token as used (correct)
            sstore(slot, 1)
            
            // BUG: Developer DUPs token for logging but the DUP'd copy
            // remains on the stack after the log operation
            // ... (intermediate operations that don't consume the copy)
            
            // Later code path accidentally uses the residual token copy
            // as input to another authorization check, effectively replaying
            // the one-time token within the same transaction frame.
        }
    }
}

Mitigations

ThreatMitigationImplementation
T1: Wrong DUP index in assemblyUse named variables in Yul instead of raw stack manipulationWrite let x := calldataload(0) in Yul; the compiler manages DUP indices. Avoid raw opcode assembly for complex logic.
T1: Off-by-one DUP errorsComment stack layout at every instruction in handwritten assemblyAnnotate each line with // Stack: [top, second, third, ...] to track depth manually. Use assembly testing frameworks (Foundry’s vm.expectRevert).
T2: Stack underflow in conditional pathsEnsure all code paths produce identical stack effectsEvery branch of an if/else in assembly must push and pop the same number of items. Use Yul’s structured control flow instead of raw JUMP/JUMPI.
T2: Underflow diagnosticsUse opcode-level tracing for debuggingFoundry’s forge test -vvvv or cast run --trace to identify the exact instruction causing the halt.
T3: Stack overflowAvoid accumulating DUP’d values in loops; POP after useExplicitly POP intermediate values after each loop iteration. Use memory for values that persist across iterations.
T3: User-controlled loop boundsValidate iteration counts against safe stack depth marginsrequire(count <= MAX_BATCH_SIZE) before entering assembly loops. Cap at a value that keeps peak stack depth well below 1024.
T4: Compiler optimization bugsPin compiler versions; verify bytecode against known-good hashesUse solc --metadata to record exact compiler version. Compare deployed bytecode against local compilation output. Monitor Solidity/Vyper security advisories.
T4: Compiler version risksAvoid compiling with unaudited optimizer versionsWait for community validation before adopting new compiler versions for production. Test with optimizer both on and off.
T5: Residual sensitive valuesPOP values immediately after final useAfter consuming a one-time value, explicitly pop() it from the stack. In Yul, let variables go out of scope to trigger implicit cleanup.
General: Assembly correctnessPrefer high-level Solidity over assembly when possibleReserve inline assembly for proven gas-critical paths. Use Solidity’s optimizer before resorting to hand-optimization.
General: Static analysisUse bytecode-level static analysis toolsTools like Mythril, Slither, and EVM symbolic executors can detect unreachable code paths and potential underflow/overflow conditions.

Compiler/EIP-Based Protections

  • Solidity >= 0.8.0: Improved stack layout management in the compiler reduces the likelihood of DUP-related code generation bugs. The IR-based codegen (via Yul) uses named variables internally, minimizing raw stack manipulation errors.
  • EIP-5450 (EOF Stack Validation): Proposes deploy-time static analysis of stack depth across all code paths. Under this EIP, contracts that could trigger DUP underflow or overflow would be rejected at deployment, eliminating runtime exceptional halts for validated code.
  • EIP-663 (DUPN/SWAPN/EXCHANGE): Extends stack access beyond 16 items with immediate operands, reducing the need to spill variables to memory and decreasing the surface area for memory-corruption bugs caused by stack depth limitations.
  • Yul intermediate representation: Solidity’s Yul IR compiles named variables to DUP/SWAP sequences automatically, providing a safety layer between developer intent and opcode-level stack manipulation. Using Yul over raw assembly preserves optimizability while reducing DUP index errors.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighMediumInline assembly bugs in DEX routers and MEV bots (recurring audit findings)
T2Smart ContractMediumLowExceptional halts in handwritten assembly (audit findings, no major exploit)
T3Smart ContractLowLowCall depth attacks (pre-EIP-150); data stack overflow is theoretically possible but practically rare
T4Smart ContractHighLowVyper nonreentrant bug → Curve Finance ($70M+); Solidity verbatim deduplication (patched pre-exploit)
T5Smart ContractMediumLowNonce/token replay patterns in assembly (audit findings)
P1ProtocolLowN/A
P2ProtocolMediumN/AEIP-663/8024/7912 under active development
P3ProtocolLowN/AEIP-5450 EOF stack validation proposal
P4ProtocolLowN/A

OpcodeRelationship
SWAP1-SWAP16 (0x90-0x9F)SWAP exchanges two stack positions; DUP clones one. Together they form the EVM’s complete stack manipulation primitive set. Wrong SWAP index causes the same silent value-substitution bugs as wrong DUP index.
PUSH0-PUSH32 (0x5F-0x7F)PUSH adds new values to the stack; DUP clones existing values. Every PUSH shifts DUP indices by 1 for all values below it, making PUSH/DUP interaction the primary source of off-by-one stack errors in handwritten assembly.
POP (0x50)POP removes the top stack item. DUP and POP are complementary: DUP grows the stack by 1, POP shrinks it by 1. Failing to POP after DUP causes stack growth that can lead to overflow; failing to DUP before POP loses a value permanently.
MLOAD (0x51)Alternative to DUP for value reuse: store to memory with MSTORE, retrieve with MLOAD. More expensive (3+3 gas minimum vs. 3 gas for DUP) but supports arbitrary depth and named slots, reducing index errors.
MSTORE (0x52)MSTORE writes a stack value to memory. When DUP’s 16-item depth limit is insufficient, spilling to memory via MSTORE is the standard workaround. EIP-663 (DUPN) would reduce the need for this pattern.