Opcode Summary

PropertyValue
Opcodes0x90 – 0x9F
MnemonicsSWAP1, SWAP2, SWAP3, … SWAP16
Gas3 (each)
Stack InputSWAPn requires at least (n + 1) items on the stack
Stack OutputThe top stack item and the (n+1)th item are exchanged; stack depth is unchanged
BehaviorSWAPn exchanges the value at the top of the stack (position 0) with the value at position n. For SWAP1 (0x90), position 1; for SWAP16 (0x9F), position 16. No other stack positions are affected. If the stack has fewer than (n + 1) items, execution reverts with a stack underflow exception.
OpcodeMnemonicSwap Positions
0x90SWAP1top ↔ 2nd
0x91SWAP2top ↔ 3rd
0x92SWAP3top ↔ 4th
0x93SWAP4top ↔ 5th
0x94SWAP5top ↔ 6th
0x95SWAP6top ↔ 7th
0x96SWAP7top ↔ 8th
0x97SWAP8top ↔ 9th
0x98SWAP9top ↔ 10th
0x99SWAP10top ↔ 11th
0x9ASWAP11top ↔ 12th
0x9BSWAP12top ↔ 13th
0x9CSWAP13top ↔ 14th
0x9DSWAP14top ↔ 15th
0x9ESWAP15top ↔ 16th
0x9FSWAP16top ↔ 17th

Threat Surface

SWAP opcodes are the only mechanism the EVM provides for reordering values already on the stack. Every function call, every conditional branch, and every multi-argument opcode like CALL (7 parameters), DELEGATECALL (6 parameters), or CREATE2 (4 parameters) depends on the stack being arranged in exactly the right order. The Solidity compiler emits hundreds of SWAP instructions in a typical contract to shuffle arguments into position, and a single wrong SWAP index — whether introduced by the compiler, a hand-written assembly block, or a bytecode-level optimizer — silently reorders parameters without any type system to catch the mistake.

The threat surface centers on three properties:

  1. A wrong SWAP index is a silent, type-free parameter reorder. The EVM stack is untyped — every element is a raw 256-bit word. SWAP3 where SWAP2 was intended doesn’t cause a revert or an error; it simply puts a different 256-bit value on top. If this value happens to be an address used as the target of a CALL, the contract calls the wrong address. If it’s a uint256 amount, the contract transfers the wrong amount. There is no runtime signal that the wrong swap occurred. This makes SWAP bugs among the hardest to detect through testing alone, since they only manifest when specific stack states expose the reordering.

  2. SWAP is critical for CALL/DELEGATECALL argument ordering. The CALL opcode consumes 7 stack items (gas, addr, value, argsOffset, argsLength, retOffset, retLength). A single SWAP error can interchange the target address with the value, or swap argsOffset with retOffset. In DELEGATECALL contexts where the callee executes in the caller’s storage, sending execution to the wrong address or with the wrong calldata can immediately drain funds or brick the contract’s storage. Hand-written assembly that constructs CALL arguments is particularly prone to SWAP-index errors because the programmer must mentally track 7+ stack positions.

  3. Stack underflow from SWAP is a DoS vector and a correctness hazard. If a SWAP instruction requires more items than are on the stack, execution reverts. An attacker who can influence a code path such that a SWAP is reached with insufficient stack depth can force transaction reverts. More subtly, if a compiler or optimizer incorrectly assumes a certain stack depth at a SWAP site — for example, after dead code elimination or inlining changes the control flow — the generated bytecode can contain SWAP instructions that underflow on paths the compiler didn’t anticipate.


Smart Contract Threats

T1: Wrong SWAP Index Reordering Function Arguments (Critical)

When the Solidity compiler or a hand-written assembly block uses the wrong SWAPn variant to arrange arguments for a function call or opcode, parameters are silently interchanged. The EVM has no type system to distinguish an address from a uint256 from a bytes32 — they are all 256-bit words:

  • CALL argument transposition. The CALL opcode expects gas, addr, value, argsOffset, argsLength, retOffset, retLength in that stack order. Using SWAP4 instead of SWAP5 when arranging these arguments can interchange the target address with the ETH value. If the address is 0x000...dead (a small number) and the value is 1000000000000000000 (1 ETH in wei, a large number), the contract attempts to CALL address 0x000...0de0b6b3a7640000 with zero value — silently sending the call to a dead address with no ETH. The transaction succeeds (CALL to a nonexistent address returns 1 with no code execution), the ETH stays in the contract, and the intended recipient never gets paid.

  • DELEGATECALL target confusion. In proxy patterns, a SWAP error that substitutes the implementation address with a calldata offset means the proxy delegates to an arbitrary address (potentially attacker-controlled if the offset value maps to a deployed contract). The delegated code executes in the proxy’s storage context, enabling full contract takeover.

  • STATICCALL return buffer misrouting. If retOffset and retLength are swapped with argsOffset and argsLength, the contract writes return data to the wrong memory location, potentially overwriting pending calldata for a subsequent call.

Why it matters: A single off-by-one in a SWAP index can redirect funds, change call targets, or corrupt memory layout. These bugs are invisible at the Solidity source level and only manifest in the generated bytecode.

T2: Stack Underflow Causing Transaction Revert (High)

SWAPn requires at least (n + 1) items on the stack. Reaching a SWAP instruction with insufficient stack depth causes an immediate revert:

  • Dead code path activation. A compiler optimization removes a PUSH instruction on a code path it considers unreachable. If an attacker can reach that path (e.g., through an unexpected fallback or a reentrancy callback), the stack is shallower than the SWAP expects, reverting the transaction. This is a DoS vector for any function that reaches the affected SWAP.

  • Fallback function stack assumptions. Contracts with hand-written assembly fallback functions often construct complex stack layouts. If the fallback is called with unexpected calldata lengths or via STATICCALL (which doesn’t push a value argument), the stack may be one item shorter than the assembly expects, causing a SWAP to underflow.

  • Optimizer-introduced underflow. The Solidity compiler’s stack optimizer aggressively reuses stack slots and eliminates redundant DUP/SWAP sequences. Bugs in the optimizer (such as issue #11312, where the compiler generated an invalid SWAP31 request) can produce bytecode containing SWAP instructions that reference positions beyond the 16-slot limit or beyond the actual stack depth.

Why it matters: Stack underflow reverts are not caught by Solidity’s type system or standard test suites. They appear only on specific execution paths and can lock funds in contracts if they affect withdrawal functions.

T3: Parameter Confusion in Assembly-Level CALL Construction (Critical)

Hand-written assembly is frequently used for gas-optimized token transfers, proxy patterns, and flash loan callbacks. Constructing a CALL’s 7-argument stack layout manually requires precise SWAP choreography:

  • Source/destination address swap in token transfers. A common assembly pattern for ERC-20 transferFrom(from, to, amount) builds the calldata in memory and then calls the token contract. If the assembly uses SWAP1 where SWAP2 was needed to arrange from and to, the contract transfers tokens from the wrong address to the wrong recipient. In a DEX context, this means the pool sends tokens to itself instead of the user, or the user’s tokens are sent to the pool’s address (effectively burned).

  • Gas and address confusion. In a low-level CALL, the gas argument sits directly above the target address on the stack. A missing or extra SWAP between these two values sends the call with the address as the gas limit (likely an astronomically large number, consuming all available gas) and the gas limit as the address (likely a small number pointing to a precompile or nonexistent account).

  • Delegatecall in proxy assembly. Minimal proxy contracts (EIP-1167 clones) and custom proxies often implement the delegatecall dispatch in raw assembly. The DELEGATECALL opcode takes 6 stack arguments. A single SWAP error can route delegation to an attacker-controlled address if a calldata pointer value happens to be an address the attacker deployed a contract to.

Why it matters: Assembly-level CALL construction is used in the highest-value contracts (DEX routers, lending pools, bridges). Manual stack manipulation bypasses all compiler safety checks.

T4: Compiler Optimizer Bugs in SWAP Generation (High)

The Solidity compiler has documented bugs where optimizer passes produce incorrect stack layouts or invalid SWAP instructions:

  • FullInliner argument evaluation order bug (SOL-2023-2). The FullInliner optimizer step, when processing code not in expression-split form, could reorder function argument evaluations. Since argument evaluation in Yul follows right-to-left order, and the FullInliner imposed an explicit left-to-right assignment to temporary variables, the resulting SWAP/DUP sequences produced incorrect stack layouts when arguments had side effects. Fixed in Solidity 0.8.21 but affected versions 0.6.7 through 0.8.20 with custom optimizer step sequences.

  • VerbatimInvalidDeduplication (SOL-2023-3). The Block Deduplicator optimizer incorrectly treated all verbatim assembly items as identical. When multiple verbatim blocks with different content were surrounded by identical SWAP/DUP instructions, the deduplicator merged them into a single block, producing incorrect bytecode. Affected Solidity 0.8.5+ with optimizer enabled.

  • Invalid SWAP request beyond depth 16 (issue #11312). The legacy code generator produced SWAP instructions referencing positions beyond the 16-slot limit (e.g., SWAP31), which is not a valid EVM opcode. This caused either compiler crashes or, in pathological cases, incorrect bytecode if error handling was bypassed.

  • Memory-safe assembly optimization regression (issue #14934). The memory-safe assembly annotation can worsen optimizations by up to 16.6%, causing the optimizer to emit excessive DUP/SWAP/POP sequences for stack shuffling. While this is a gas overhead rather than a correctness bug, the additional stack manipulation increases the attack surface for optimizer edge cases.

Why it matters: Compiler bugs that affect SWAP generation can silently alter the behavior of contracts that compile correctly from source. Auditors reviewing Solidity source may miss vulnerabilities that exist only in the generated bytecode.

T5: Argument Order Transposition in DELEGATECALL and CALL Dispatch (Critical)

Multi-argument opcodes are especially vulnerable to SWAP transposition because the semantic meaning of each position is invisible at the bytecode level:

  • DELEGATECALL storage corruption. DELEGATECALL(gas, addr, argsOffset, argsLength, retOffset, retLength) executes the code at addr in the caller’s context. If a SWAP error swaps addr with argsOffset, the EVM delegates to whatever address the memory offset value represents. If that address has code (which is probabilistically unlikely but becomes feasible if the attacker can deploy contracts at predictable addresses via CREATE2), the attacker’s code executes in the victim’s storage context.

  • CREATE2 salt/initcode confusion. CREATE2(value, offset, length, salt) deploys a contract. Swapping salt with offset or length changes both the deployment address and the deployed bytecode. An attacker who can influence the salt value (e.g., through a user-supplied parameter) could cause deployment to a predictable address where they’ve pre-deployed a malicious contract.

  • LOG topic/data confusion. LOG0-LOG4 opcodes expect (offset, length, topic0, …, topicN) on the stack. A SWAP error that moves a topic into the data position or vice versa produces events with incorrect indexing. While not directly exploitable for fund theft, incorrect event emission can corrupt off-chain indexers, subgraphs, and monitoring systems that protocols depend on for security alerts.

Why it matters: Every multi-argument EVM opcode is a potential site for SWAP transposition. The risk scales with the number of arguments — CALL’s 7 parameters create 21 possible pairwise transpositions from a single wrong SWAP.


Protocol-Level Threats

P1: No Direct DoS Vector from SWAP Gas Cost (Low)

All SWAP variants cost a fixed 3 gas with no dynamic component. A SWAP instruction reads two stack positions and writes two stack positions — all in the EVM’s internal memory, with no storage, memory expansion, or external call. SWAP cannot be used for gas griefing. Even a sequence of 1000 SWAP operations costs only 3000 gas.

P2: Stack Depth Limit as a Consensus Constraint (Low)

The EVM enforces a maximum stack depth of 1024 items, but SWAP can only reach 16 deep (SWAP16 accesses position 16). This means SWAP cannot cause stack overflow. The 16-slot reach limit is a hard constraint of the instruction set, not a client implementation detail, so all EVM clients agree on SWAP behavior. No consensus bugs have been attributed to SWAP semantics.

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

Several EIPs propose extending stack manipulation beyond the current 16-slot limit:

  • EIP-663 (SWAPN, DUPN, EXCHANGE) introduces immediate-argument variants that can access arbitrary stack depths. SWAPN takes a 1-byte immediate specifying the swap depth (0-255), dramatically expanding the reachable stack. This removes the “stack too deep” compiler limitation but also increases the attack surface — a wrong immediate value in SWAPN can reach far deeper into the stack than SWAP16 ever could.

  • EIP-7912 and EIP-8024 propose similar pragmatic stack manipulation tools with backward compatibility. Both enable compilers to produce more efficient code but introduce new classes of stack-reordering bugs at depths that were previously impossible.

If adopted, these EIPs will require auditors and formal verification tools to handle stack access patterns at arbitrary depths, rather than the current bounded 16-slot range.

P4: SWAP Across Hard Forks (Low)

SWAP1-SWAP16 semantics have never changed across any Ethereum hard fork. They were part of the original Frontier instruction set (2015) and have been stable since. The only related changes are the proposed EIP-663/7912/8024 extensions, which add new opcodes rather than modifying existing SWAP behavior.


Edge Cases

Edge CaseBehaviorSecurity Implication
SWAP1 with exactly 2 stack itemsSucceeds; exchanges the two itemsMinimum valid stack depth for SWAP1
SWAP16 with exactly 17 stack itemsSucceeds; exchanges positions 0 and 16Minimum valid stack depth for SWAP16
SWAPn with n stack items (underflow)Immediate revert; all gas consumed for the call frameDoS if reachable on critical paths (withdrawals, liquidations)
SWAP1 where both items are identicalSucceeds; stack is unchanged (no-op)No security implication, but optimizer may remove it, altering gas profile
SWAP in unreachable codeNever executes; but invalid SWAP opcodes (0xA0+) would be treated as INVALIDDead code containing SWAP is benign; but if “dead” code becomes reachable, underflows surface
SWAP after JUMP to wrong JUMPDESTStack layout at the JUMPDEST may differ from expectationWrong SWAP index at a JUMPDEST reached from multiple paths can reorder different arguments depending on the caller
Consecutive SWAP1 SWAP1Swaps twice, restoring original order (self-canceling)Optimizer removes these; if removal fails, 6 gas wasted but no correctness issue
SWAP16 near max stack depth (1024)Succeeds as long as stack has ≥ 17 itemsNo overflow risk; SWAP doesn’t push or pop
SWAP in STATICCALL contextBehaves identically; SWAP only reorders stack, no state changeNo special interaction with read-only context
SWAP with dirty upper bitsAll 256 bits are preserved; no masking or truncationIf a value had dirty upper bits (e.g., an address with non-zero upper 96 bits), SWAP preserves the dirty bits in their new position

Real-World Exploits

Exploit 1: FullInliner Argument Evaluation Order Bug — Silent Parameter Reordering (July 2023)

Root cause: The Solidity compiler’s FullInliner optimizer step reordered function argument evaluations when processing Yul code not in expression-split form, producing SWAP/DUP sequences that placed arguments in the wrong stack positions.

Details: Yul function arguments are evaluated right-to-left, and the results are expected on the stack in a specific order. The FullInliner, when inlining a function call, assigned argument expressions to temporary variables in a left-to-right order. If argument expressions had side effects (e.g., calling a function that modifies state or reads from a sequence-dependent source), the reordering changed which values ended up in which stack positions. The resulting bytecode contained SWAP sequences that arranged incorrectly-evaluated arguments, producing contracts whose behavior silently diverged from the source code.

The bug affected Solidity versions 0.6.7 through 0.8.20 but only triggered when all of these conditions were met: (1) the Yul optimizer was active, (2) a custom optimizer step sequence was used (not the default), (3) the FullInliner ran before ExpressionSplitter, and (4) the code contained inline assembly or was pure Yul with side-effecting argument expressions. The default optimizer sequence was safe because ExpressionSplitter always ran before FullInliner.

SWAP’s role: The FullInliner produces temporary variable assignments that the code generator translates into DUP/SWAP sequences. The incorrect evaluation order meant these DUP/SWAP sequences placed values in the wrong stack positions — the generated SWAPs were syntactically valid but semantically wrong, silently reordering function arguments.

Impact: Classified as “low” overall risk by the Solidity team (high severity if triggered, but very low likelihood due to the narrow trigger conditions). No known exploited contracts, but the bug affected all contracts compiled with custom optimizer sequences over a 3-year window. Etherscan maintains a bug database entry for FullInlinerNonExpressionSplitArgumentEvaluationOrder.

References:


Exploit 2: 1inch Fusion v1 — Calldata Corruption Leading to Parameter Substitution ($5M, March 2025)

Root cause: Assembly-level parameter handling in the 1inch Fusion v1 resolver contract failed to validate an interactionLength value read from calldata. An attacker supplied a crafted negative value that caused an integer underflow in offset calculations, corrupting the memory layout used to construct a low-level CALL.

Details: The 1inch Fusion v1 contracts used hand-written assembly to parse order data from calldata and construct CALL instructions for resolver interactions. The interactionLength parameter, read from user-supplied calldata, was used to calculate memory offsets for the resolver address “suffix.” By providing a value that underflowed (interpreted as -512 in two’s complement), the attacker shifted the memory write position for the suffix, effectively overwriting the resolver address with an attacker-controlled address.

The corrupted memory layout meant that when the contract constructed the CALL opcode’s stack arguments (gas, addr, value, argsOffset, argsLength, retOffset, retLength), the addr parameter pointed to the attacker’s contract instead of the legitimate resolver. The CALL transferred user-approved tokens to the attacker’s address.

SWAP’s role: The vulnerability is a direct consequence of the fragility inherent in hand-written assembly that manually arranges CALL stack arguments. The CALL opcode requires 7 parameters in exact stack order. The contract’s assembly used a series of PUSH, DUP, and SWAP instructions to arrange these parameters, and the corrupted memory offset propagated through the SWAP-based stack arrangement into the wrong argument position. The attack demonstrates how a single corrupted value in a SWAP-dependent stack layout can redirect an entire CALL.

Impact: ~$5M stolen from 1inch Fusion v1 users. The attacker exploited the corrupted resolver address to redirect transferFrom operations, stealing tokens that users had approved for the 1inch protocol.

References:


Exploit 3: Head Overflow Bug in Calldata Tuple ABI-Reencoding — Compiler-Level Stack Corruption (August 2022)

Root cause: The Solidity ABI coder v2 generated incorrect memory writes when re-encoding calldata tuples containing both dynamic and static components, producing corrupted function arguments in external calls.

Details: When the ABI encoder re-encoded a calldata tuple (e.g., for forwarding to an external call), it performed an overly aggressive cleanup on the last static component. A single mstore() instruction intended to zero-pad a static array instead overwrote 32 bytes belonging to the first dynamic component’s head pointer. This meant that the external call received corrupted calldata where the dynamic component’s offset pointed to the wrong memory location.

The bug affected Solidity 0.5.8 through 0.8.15 and triggered when: (1) ABI coder v2 was used (default since 0.8.0), (2) a tuple contained at least one dynamic component followed by a statically-sized calldata array of uint or bytes32, and (3) the tuple was re-encoded for an external call, abi.encode, or event emission.

SWAP’s role: The ABI encoder generates SWAP-heavy code to arrange memory pointers, lengths, and data for encoding. The head overflow occurred because the generated SWAP/DUP sequence for the cleanup step operated on the wrong memory offset — the compiler assumed the cleanup target was isolated, but the tight memory packing meant the SWAP-arranged pointer overlapped with the dynamic component’s head. The resulting external CALL received arguments where one parameter was silently corrupted by another.

Impact: Classified as “medium” severity by the Solidity team. Affected all contracts compiled with Solidity 0.5.8–0.8.15 using ABI coder v2 with the specific tuple pattern. No confirmed exploits in the wild, but the 3-year exposure window and the default-on status of ABI coder v2 meant a significant number of deployed contracts were potentially affected.

References:


Attack Scenarios

Scenario A: Wrong SWAP Index in Assembly-Level Token Transfer

contract VulnerableRouter {
    function swapExactTokens(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        address recipient
    ) external {
        // Transfer tokenIn from sender to this contract
        assembly {
            // Build transferFrom(msg.sender, address(this), amountIn) calldata
            let ptr := mload(0x40)
            mstore(ptr, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
            mstore(add(ptr, 0x04), caller())
            mstore(add(ptr, 0x24), address())
            mstore(add(ptr, 0x44), amountIn)
 
            let success := call(gas(), tokenIn, 0, ptr, 0x64, 0, 0x20)
            if iszero(success) { revert(0, 0) }
 
            // Now swap via the pool and send tokenOut to recipient.
            // BUG: should be swap(2, 1) to get (recipient, tokenOut)
            // but uses swap(1, 2) -- recipient and tokenOut are transposed.
            // The contract calls pool.swap() with tokenOut as the "recipient"
            // (interpreted as an address -- the token contract itself)
            // and recipient as the "token" (the user's address, which has no code).
            // Result: tokens are sent to the token contract address, lost forever.
        }
    }
}
 
// In correct bytecode, the stack before the pool CALL should be:
//   [gas, poolAddr, 0, argsOffset, argsLen, retOffset, retLen]
// A single wrong SWAP makes it:
//   [gas, poolAddr, 0, argsOffset, argsLen, retLen, retOffset]
// Now return data overwrites calldata memory, and the next call reads garbage.

Scenario B: DELEGATECALL Target Confusion via SWAP Transposition

contract MinimalProxy {
    // Hand-written assembly proxy -- real pattern from EIP-1167 variants
    fallback() external payable {
        assembly {
            let impl := sload(0) // implementation address from slot 0
 
            // Copy calldata to memory
            calldatacopy(0, 0, calldatasize())
 
            // BUG: SWAP error transposes impl address with calldatasize()
            // Intended: delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            // Actual:   delegatecall(gas(), calldatasize(), 0, impl, 0, 0)
            //
            // calldatasize() is a small number (e.g., 0x44 = 68)
            // If an attacker deploys a contract at address 0x44 via CREATE2,
            // the proxy delegates to the attacker's code in the proxy's storage.
            let result := delegatecall(gas(), calldatasize(), 0, impl, 0, 0)
 
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}
 
// Attack: Deploy malicious contract at the predictable address
// matching calldatasize() (e.g., 0x44). Call the proxy with 68 bytes
// of calldata. The proxy delegates to the attacker's contract,
// which runs selfdestruct or overwrites the implementation slot.

Scenario C: Stack Underflow DoS on Withdrawal Function

contract StakingPool {
    mapping(address => uint256) public stakes;
 
    function withdraw(uint256 amount) external {
        require(stakes[msg.sender] >= amount, "insufficient");
        stakes[msg.sender] -= amount;
 
        // Assembly-optimized ETH transfer
        assembly {
            // On a specific code path (e.g., when amount == balance),
            // the optimizer removes a DUP that was feeding a later SWAP.
            // The SWAP now reaches one position too deep -> underflow -> revert.
            //
            // Normal path: stack has [caller, amount, ...] -> SWAP1 works
            // Edge path:   optimizer eliminated the DUP -> stack has [amount]
            //              SWAP1 underflows -> revert
            let ok := call(gas(), caller(), amount, 0, 0, 0, 0)
            if iszero(ok) { revert(0, 0) }
        }
    }
 
    // Impact: Users whose withdrawal amount equals their exact balance
    // hit the optimizer edge case and can never withdraw.
    // Funds are permanently locked.
}

Scenario D: Event Topic/Data Transposition

contract TokenBridge {
    event Transfer(address indexed from, address indexed to, uint256 amount);
 
    function bridgeOut(address to, uint256 amount) external {
        // ... bridge logic ...
 
        assembly {
            // LOG3 expects: offset, length, topic0, topic1, topic2
            // BUG: SWAP error puts `amount` as topic2 and `to` as the data.
            // Events are emitted with the recipient address in the data field
            // (non-indexed) and the amount as an indexed topic.
            //
            // Off-chain indexers tracking Transfer events parse topic2 as `to`
            // (an address) but receive `amount` (a uint256) instead.
            // The bridge's relayer misinterprets the event and credits the
            // wrong recipient on the destination chain.
            let sig := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
            log3(0x00, 0x20, sig, caller(), amount) // amount where `to` should be
            mstore(0x00, to)                         // `to` in data where amount should be
        }
    }
 
    // Impact: Bridge relayer reads topic2 as destination address,
    // gets a large uint256 (amount) interpreted as an address.
    // Tokens are minted to a random/nonexistent address on L2.
}

Mitigations

ThreatMitigationImplementation
T1: Wrong SWAP index reordering argumentsAvoid hand-written assembly for multi-argument opcodesUse Solidity’s high-level address.call{value: v}(data) syntax; let the compiler generate the SWAP sequences
T1: Parameter transpositionBytecode-level formal verificationUse tools like KEVM, EVM-Huff formal verifier, or Certora to verify that stack layouts at CALL sites match specifications
T2: Stack underflow DoSComprehensive path coverage in testingUse symbolic execution (Mythril, Manticore) to explore all reachable stack states at every SWAP instruction
T2: Optimizer-introduced underflowPin compiler versions; test with optimizer on and offCompare bytecode output between optimizer settings; use solc --asm to inspect generated SWAP sequences
T3: Assembly CALL construction errorsEncapsulate assembly CALL patterns in tested librariesUse audited libraries (Solady, OpenZeppelin) for low-level calls; never write raw CALL assembly in application code
T3: Gas/address confusion in CALLComment stack state at every SWAP in assemblyAdd inline comments documenting the expected stack layout before and after each SWAP instruction
T4: Compiler optimizer bugsUse default optimizer sequences; update compiler promptlyAvoid custom optimizer step sequences; monitor Solidity security advisories; check Etherscan’s solcbuginfo database
T4: FullInliner argument reorderEnsure ExpressionSplitter runs before FullInlinerUse the default optimizer sequence, which always runs ExpressionSplitter first
T5: DELEGATECALL target confusionValidate implementation addresses before delegatecallrequire(impl.code.length > 0) before DELEGATECALL; use immutable implementation slots
T5: CREATE2 salt confusionValidate all CREATE2 parameters explicitlyCheck that salt, offset, and length are in the expected ranges before executing CREATE2
General: Stack layout verificationUse stack-layout-aware static analysisTools like Slither, Securify2, and custom Yul analyzers can flag SWAP sequences that don’t match expected argument orderings

Compiler/EIP-Based Protections

  • Solidity >= 0.8.21: Fixes the FullInliner argument evaluation order bug. All contracts using custom optimizer sequences should recompile.
  • Solidity >= 0.8.16: Fixes the head overflow bug in calldata tuple ABI-reencoding that corrupted CALL arguments.
  • Solidity >= 0.8.24: Fixes the VerbatimInvalidDeduplication bug that could merge different verbatim blocks surrounded by identical SWAP/DUP patterns.
  • EIP-663 / EIP-7912 / EIP-8024 (Proposed): Introduce SWAPN/DUPN/EXCHANGE with immediate arguments, eliminating “stack too deep” errors. However, these expand the possible swap-depth range from 16 to 255, requiring updated tooling.
  • Solidity --via-ir pipeline: The intermediate representation pipeline uses a more principled stack scheduling algorithm than the legacy code generator, reducing (but not eliminating) the risk of invalid SWAP generation. However, it introduces its own class of “stack too deep” errors for complex contracts.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalMediumFullInliner bug (SOL-2023-2); 1inch Fusion calldata corruption ($5M)
T2Smart ContractHighMediumSolidity compiler issue #11312 (invalid SWAP31); stack-too-deep failures in SushiSwap Trident, Gnosis Safe
T3Smart ContractCriticalHigh1inch Fusion v1 ($5M); hand-written assembly CALL construction is pervasive in DEX routers
T4Smart ContractHighLowFullInliner (SOL-2023-2), VerbatimDedup (SOL-2023-3), Head Overflow (0.5.8-0.8.15)
T5Smart ContractCriticalMediumDELEGATECALL storage slot collisions (Parity $30M); CREATE2 address prediction attacks
P1ProtocolLowN/A
P2ProtocolLowN/A
P3ProtocolMediumLowEIP-663/7912/8024 proposals expanding SWAP reach to 255 depth
P4ProtocolLowN/A

OpcodeRelationship
DUP1-DUP16 (0x80-0x8F)DUP duplicates a stack item to the top; SWAP reorders without duplicating. DUP/SWAP are always used together to arrange multi-argument opcode inputs. A wrong DUP index has the same silent-reordering risk as a wrong SWAP index.
PUSH1-PUSH32 (0x60-0x7F)PUSH places new values onto the stack that SWAP then reorders. The PUSH+SWAP pattern is the fundamental mechanism for arranging opcode arguments. An incorrect PUSH value combined with a correct SWAP is indistinguishable from a correct PUSH with a wrong SWAP.
POP (0x50)POP removes the top stack item. POP after SWAP is used to discard values after reordering. A missing POP after a SWAP leaves an extra item on the stack, shifting all subsequent SWAP indices by one.
CALL (0xF1)CALL consumes 7 stack arguments — the most of any EVM opcode. Every CALL site requires precise SWAP choreography to place gas, address, value, and memory pointers in order. CALL is the primary consumer of complex SWAP sequences.
DELEGATECALL (0xF4)DELEGATECALL consumes 6 stack arguments and executes in the caller’s storage context. A SWAP error in DELEGATECALL arguments is strictly more dangerous than in CALL because the wrong code runs with the caller’s state.
STATICCALL (0xFA)STATICCALL consumes 6 stack arguments like DELEGATECALL. The read-only context limits damage from SWAP errors (no state modification), but incorrect return data routing still corrupts memory.
CREATE2 (0xF5)CREATE2 consumes 4 stack arguments including a salt that determines the deployed address. A SWAP error swapping salt with initcode length changes the deployment address, potentially deploying to an attacker-predictable location.