Opcode Summary
| Property | Value |
|---|---|
| Opcodes | 0x90 – 0x9F |
| Mnemonics | SWAP1, SWAP2, SWAP3, … SWAP16 |
| Gas | 3 (each) |
| Stack Input | SWAPn requires at least (n + 1) items on the stack |
| Stack Output | The top stack item and the (n+1)th item are exchanged; stack depth is unchanged |
| Behavior | SWAPn 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. |
| Opcode | Mnemonic | Swap Positions |
|---|---|---|
| 0x90 | SWAP1 | top ↔ 2nd |
| 0x91 | SWAP2 | top ↔ 3rd |
| 0x92 | SWAP3 | top ↔ 4th |
| 0x93 | SWAP4 | top ↔ 5th |
| 0x94 | SWAP5 | top ↔ 6th |
| 0x95 | SWAP6 | top ↔ 7th |
| 0x96 | SWAP7 | top ↔ 8th |
| 0x97 | SWAP8 | top ↔ 9th |
| 0x98 | SWAP9 | top ↔ 10th |
| 0x99 | SWAP10 | top ↔ 11th |
| 0x9A | SWAP11 | top ↔ 12th |
| 0x9B | SWAP12 | top ↔ 13th |
| 0x9C | SWAP13 | top ↔ 14th |
| 0x9D | SWAP14 | top ↔ 15th |
| 0x9E | SWAP15 | top ↔ 16th |
| 0x9F | SWAP16 | top ↔ 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:
-
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.
-
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.
-
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, retLengthin that stack order. Using SWAP4 instead of SWAP5 when arranging these arguments can interchange the target address with the ETH value. If the address is0x000...dead(a small number) and the value is1000000000000000000(1 ETH in wei, a large number), the contract attempts to CALL address0x000...0de0b6b3a7640000with 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 arrangefromandto, 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
verbatimassembly items as identical. When multipleverbatimblocks 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-safeassembly 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
addrin the caller’s context. If a SWAP error swapsaddrwithargsOffset, 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
saltwithoffsetorlengthchanges 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 Case | Behavior | Security Implication |
|---|---|---|
| SWAP1 with exactly 2 stack items | Succeeds; exchanges the two items | Minimum valid stack depth for SWAP1 |
| SWAP16 with exactly 17 stack items | Succeeds; exchanges positions 0 and 16 | Minimum valid stack depth for SWAP16 |
| SWAPn with n stack items (underflow) | Immediate revert; all gas consumed for the call frame | DoS if reachable on critical paths (withdrawals, liquidations) |
| SWAP1 where both items are identical | Succeeds; stack is unchanged (no-op) | No security implication, but optimizer may remove it, altering gas profile |
| SWAP in unreachable code | Never executes; but invalid SWAP opcodes (0xA0+) would be treated as INVALID | Dead code containing SWAP is benign; but if “dead” code becomes reachable, underflows surface |
| SWAP after JUMP to wrong JUMPDEST | Stack layout at the JUMPDEST may differ from expectation | Wrong SWAP index at a JUMPDEST reached from multiple paths can reorder different arguments depending on the caller |
| Consecutive SWAP1 SWAP1 | Swaps 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 items | No overflow risk; SWAP doesn’t push or pop |
| SWAP in STATICCALL context | Behaves identically; SWAP only reorders stack, no state change | No special interaction with read-only context |
| SWAP with dirty upper bits | All 256 bits are preserved; no masking or truncation | If 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:
- Solidity Blog: FullInliner Non-Expression-Split Argument Evaluation Order Bug
- Etherscan Solidity Bug Database
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:
- Solidity Blog: Head Overflow Bug in Calldata Tuple ABI-Reencoding
- Eocene: Analysis of Head Overflow in ABIv2-Reencoding
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
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Wrong SWAP index reordering arguments | Avoid hand-written assembly for multi-argument opcodes | Use Solidity’s high-level address.call{value: v}(data) syntax; let the compiler generate the SWAP sequences |
| T1: Parameter transposition | Bytecode-level formal verification | Use tools like KEVM, EVM-Huff formal verifier, or Certora to verify that stack layouts at CALL sites match specifications |
| T2: Stack underflow DoS | Comprehensive path coverage in testing | Use symbolic execution (Mythril, Manticore) to explore all reachable stack states at every SWAP instruction |
| T2: Optimizer-introduced underflow | Pin compiler versions; test with optimizer on and off | Compare bytecode output between optimizer settings; use solc --asm to inspect generated SWAP sequences |
| T3: Assembly CALL construction errors | Encapsulate assembly CALL patterns in tested libraries | Use audited libraries (Solady, OpenZeppelin) for low-level calls; never write raw CALL assembly in application code |
| T3: Gas/address confusion in CALL | Comment stack state at every SWAP in assembly | Add inline comments documenting the expected stack layout before and after each SWAP instruction |
| T4: Compiler optimizer bugs | Use default optimizer sequences; update compiler promptly | Avoid custom optimizer step sequences; monitor Solidity security advisories; check Etherscan’s solcbuginfo database |
| T4: FullInliner argument reorder | Ensure ExpressionSplitter runs before FullInliner | Use the default optimizer sequence, which always runs ExpressionSplitter first |
| T5: DELEGATECALL target confusion | Validate implementation addresses before delegatecall | require(impl.code.length > 0) before DELEGATECALL; use immutable implementation slots |
| T5: CREATE2 salt confusion | Validate all CREATE2 parameters explicitly | Check that salt, offset, and length are in the expected ranges before executing CREATE2 |
| General: Stack layout verification | Use stack-layout-aware static analysis | Tools 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
verbatimblocks 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-irpipeline: 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 ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | Medium | FullInliner bug (SOL-2023-2); 1inch Fusion calldata corruption ($5M) |
| T2 | Smart Contract | High | Medium | Solidity compiler issue #11312 (invalid SWAP31); stack-too-deep failures in SushiSwap Trident, Gnosis Safe |
| T3 | Smart Contract | Critical | High | 1inch Fusion v1 ($5M); hand-written assembly CALL construction is pervasive in DEX routers |
| T4 | Smart Contract | High | Low | FullInliner (SOL-2023-2), VerbatimDedup (SOL-2023-3), Head Overflow (0.5.8-0.8.15) |
| T5 | Smart Contract | Critical | Medium | DELEGATECALL storage slot collisions (Parity $30M); CREATE2 address prediction attacks |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
| P3 | Protocol | Medium | Low | EIP-663/7912/8024 proposals expanding SWAP reach to 255 depth |
| P4 | Protocol | Low | N/A | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| 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. |