Opcode Summary

PropertyValue
Opcode0x50
MnemonicPOP
Gas2
Stack Inputa (any 32-byte value on top of the stack)
Stack Output(none — the top value is removed and discarded)
BehaviorRemoves the topmost item from the stack and discards it permanently. The value is not written to memory, storage, or returndata — it simply ceases to exist. POP is one of the simplest opcodes in the EVM: it decrements the stack pointer by one. If the stack is empty when POP executes, the EVM triggers a stack underflow exception and the entire execution context reverts, consuming all forwarded gas.

Threat Surface

POP itself is mechanically trivial — it removes one word from the stack. It has no memory side effects, no storage interaction, and no external calls. Its 2-gas cost is among the lowest in the EVM. Yet POP sits at the center of one of the most consequential bug classes in smart contract security: silently discarded return values.

The threat surface centers on three properties:

  1. POP is how the EVM discards values the programmer chose not to use. When a Solidity developer calls address.send(amount) without capturing the boolean return value, the compiler emits a CALL (which pushes a success/failure flag onto the stack) followed immediately by POP to discard that flag. The contract proceeds as if the call succeeded regardless of the actual outcome. This compiler-level pattern — CALL then POP — is the bytecode signature of the unchecked external call vulnerability (SWC-104), one of the most exploited bug classes in Ethereum’s history.

  2. Stack underflow on an empty stack is an immediate revert. If POP executes when the stack is empty, the EVM halts with an exception. All gas forwarded to the current call context is consumed, and all state changes within that context are rolled back. While the EVM’s runtime stack validation prevents undefined behavior, an attacker who can force a code path that triggers an unexpected POP on an empty stack causes a denial-of-service: the transaction reverts, and the caller loses gas. Under EOF (EIP-5450), stack underflow is validated at deploy time, eliminating this class of runtime failure for EOF-validated contracts.

  3. Inline assembly POP is unchecked and compiler-invisible. In Yul/inline assembly blocks, developers can emit raw pop instructions to discard values from function calls like call(), staticcall(), or delegatecall(). Unlike high-level Solidity, the compiler does not warn when assembly pop discards a return value. This makes assembly blocks a blind spot for static analysis tools and a frequent source of unchecked call vulnerabilities in hand-optimized contracts.


Smart Contract Threats

T1: Discarded CALL Return Values — Unchecked External Calls (Critical)

When a contract makes an external call using low-level functions (.send(), .call(), .delegatecall(), .staticcall()), the EVM pushes a boolean success flag onto the stack. If the developer does not check this flag, the Solidity compiler emits POP to discard it. The contract continues execution as though the call succeeded, even if it failed. This is classified as SWC-104 (Unchecked Call Return Value) and appears in approximately 30% of Solidity audit findings.

The consequences include:

  • Lost ETH transfers treated as successful. A contract calls recipient.send(amount) without checking the return value. The send fails (recipient’s fallback reverts, or recipient is a contract requiring >2300 gas), but the contract marks the payment as complete. The ETH remains in the contract, and the recipient never receives it. A second party can then claim those funds or the contract enters an inconsistent state.

  • ERC-20 transfers that silently fail. Some ERC-20 tokens (notably USDT/Tether) do not return a boolean from transfer() and transferFrom(). When called via low-level .call(), the return data is empty, and if not handled with abi.decode, the contract assumes success. POP discards whatever was (or wasn’t) on the stack, and the token transfer is treated as complete when zero tokens moved.

  • State corruption from partial execution. When a multi-step operation (e.g., swap then distribute proceeds) has an unchecked call midway, the later steps execute with incorrect assumptions about earlier steps’ success. State variables are updated based on phantom transfers, creating exploitable inconsistencies.

Why it matters: Unchecked external calls are a top-5 smart contract vulnerability by frequency and by dollar losses. The bytecode pattern CALL ... POP (without a conditional jump on the success flag) is the direct manifestation of this bug at the EVM level.

T2: Stack Underflow — Denial of Service (Medium)

POP on an empty stack causes an immediate exception. The EVM halts the current execution context, consumes all remaining gas forwarded to that context, and reverts all state changes. Attack vectors include:

  • Malformed bytecode execution. If a contract’s bytecode contains a code path where POP executes with insufficient stack depth (e.g., due to a compiler bug or hand-written bytecode error), any transaction hitting that path reverts unconditionally. This is a permanent denial-of-service for that code path.

  • Griefing via forced revert paths. In contracts that use assembly and depend on certain stack states, an attacker who can influence which code path executes may steer execution toward a path that underflows. The transaction reverts, wasting the caller’s gas.

  • EOF mitigation. EIP-5450 (EOF Stack Validation) moves stack underflow/overflow checks to deploy time. Under EOF, bytecode that could underflow at any reachable instruction is rejected during deployment. This eliminates runtime stack underflow for EOF contracts, though legacy (non-EOF) contracts remain vulnerable.

Why it matters: Stack underflow from POP doesn’t enable fund theft directly, but it causes gas loss and denial-of-service. In contracts where a revert blocks a critical state transition (e.g., a payment queue), underflow can freeze funds indefinitely.

T3: Compiler Optimization Discarding Needed Values (Medium)

The Solidity compiler uses POP when it determines a return value is unused. Compiler optimizations can interact with this in unexpected ways:

  • Dead code elimination removing safety checks. If the optimizer determines that a variable holding a call’s return value is never read, it may elide the variable and emit POP directly after the CALL. A developer who intended to check the return value in a later statement that the optimizer removed (e.g., due to an unreachable code path) silently loses the check.

  • Optimizer bugs affecting inline assembly. The Solidity optimizer (versions >=0.8.13) had a bug where it incorrectly removed memory writes in assembly blocks when the block didn’t reference surrounding Solidity variables. While not directly a POP issue, optimizer interactions with stack management in assembly can lead to values being discarded or overwritten unexpectedly.

  • ABI decoding edge cases. When calling contracts that return unexpected data lengths (e.g., a token that returns nothing vs. a boolean), the compiler’s generated code may POP values that were expected to be decoded, or fail to POP extra stack items, leading to stack imbalance in subsequent operations.

Why it matters: Developers trust the compiler to faithfully translate their intent. When optimizations discard values the developer assumed were retained, the resulting bytecode has different security properties than the source code suggests.

T4: Inline Assembly POP Misuse (High)

In Yul and inline assembly, pop is explicitly used to discard return values from low-level calls. Unlike high-level Solidity (where the compiler warns about unused return values), assembly pop is silent:

  • Explicit suppression of call results. A common assembly pattern is pop(call(gas(), target, value, 0, 0, 0, 0)) — the pop discards the success flag. The developer may intend a fire-and-forget call, but if the call’s success matters to subsequent logic, this creates the same vulnerability as SWC-104 at the assembly level, invisible to Solidity-level static analyzers.

  • Stack balancing errors. Assembly blocks must leave the stack in the same state they found it (or the expected output state). Incorrect use of pop to “fix” a stack imbalance can mask deeper logic errors — the developer pops a value to make the compiler happy, but that value was a critical return code or pointer.

  • Memory pointer corruption. When assembly code interacts with the free memory pointer (0x40) and uses pop to discard intermediate values, a misplaced pop can cause subsequent memory operations to read from or write to incorrect offsets, leading to data corruption that propagates through the contract.

Why it matters: Assembly is used in gas-critical code paths (DEX routers, bridges, MEV bots) where correctness is paramount. Assembly pop is the most direct way to silently ignore a call failure, and it bypasses all compiler-level safety checks.

T5: Silent Value Destruction in Complex Stack Operations (Low)

POP permanently destroys information. In complex contracts with deep stack operations, an incorrectly placed POP can silently destroy values that are needed later:

  • Discarding DUP’d values prematurely. A developer DUPs a value for later use, then inadvertently POPs the copy before it’s consumed. The subsequent opcode that expected the value reads a different stack item, causing logic corruption.

  • Off-by-one stack depth errors in hand-written bytecode. Contracts written in raw bytecode or generated by non-Solidity compilers (Vyper, Huff, Fe) may have stack depth miscalculations where POP removes the wrong item from the stack. Since POP always removes the top item, any miscalculation in the number of items pushed/popped before it changes which value gets destroyed.

  • CREATE/CREATE2 return value discard. CREATE and CREATE2 push the deployed contract’s address (or 0 on failure) onto the stack. If this value is POPped without checking for zero, the contract doesn’t know whether deployment succeeded. Subsequent interactions with the “deployed” contract target address(0), which either reverts or burns ETH.

Why it matters: While primarily a correctness issue rather than a direct attack vector, silent value destruction in production bytecode has caused contract failures where funds become permanently locked.


Protocol-Level Threats

P1: No Gas Griefing Vector (Low)

POP costs a fixed 2 gas with no dynamic component. It reads and discards one stack item in a single cycle. It cannot be used for gas amplification attacks, and its cost is too low to be meaningful in gas metering games. No protocol-level vulnerability has ever been attributed to POP’s gas cost.

P2: Consensus Safety (Low)

POP is deterministic — it removes the top stack element unconditionally. All EVM client implementations agree on its behavior. No consensus bugs have been attributed to POP. The only implementation variance is in error handling for stack underflow: all clients must revert with an out-of-gas exception, and all conforming clients do.

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

EIP-5450 introduces deploy-time stack validation for EOF-formatted contracts. Under EOF, the validator statically verifies that no instruction can execute with insufficient stack depth, which means POP on an empty stack is caught before the contract is deployed. This eliminates the T2 (stack underflow) threat for EOF contracts. However, legacy (non-EOF) contracts will coexist on the network indefinitely, so the runtime underflow risk persists for older deployed code.


Edge Cases

Edge CaseBehaviorSecurity Implication
POP on empty stackEVM exception: execution halts, all gas consumed, state revertedDenial-of-service for that code path; transaction sender loses gas
POP after failed CALLPOP removes the 0 (failure) flag from the stack; execution continuesContract proceeds as if the call succeeded — classic unchecked call bug (SWC-104)
POP after successful CALLPOP removes the 1 (success) flag; execution continuesNo immediate bug, but the success confirmation is lost — later error paths cannot distinguish success from failure
POP in STATICCALL contextBehaves identically to POP in any context; no state-change restriction applies to POP itselfDiscarding a STATICCALL return value is the same unchecked-call pattern in a read-only context
POP of CREATE/CREATE2 resultDiscards the deployed address (or 0 on failure)Contract cannot verify deployment succeeded; may interact with address(0) or a non-existent contract
POP in inline assembly (pop(call(...)))Explicitly discards the low-level call’s success booleanBypasses Solidity compiler warnings about unchecked returns; invisible to most static analyzers
Multiple consecutive POPsEach POP removes one stack item; N POPs remove N itemsStack underflow if fewer than N items remain; common in hand-written bytecode cleanup sequences
POP under EOF (EIP-5450)Stack underflow is validated at deploy time; POP on empty stack prevents deploymentEliminates runtime underflow for EOF contracts; no effect on legacy contracts
POP of high-value data (private keys, seeds)Value is removed from the stack; no memory/storage tracePOP does not zero memory — the value was never in memory. Stack values exist only in the execution context and are not recoverable after POP.

Real-World Exploits

Exploit 1: King of the Ether Throne — Unchecked send() Return Value (February 2016)

Root cause: The contract used .send() to transfer ETH compensation to dethroned monarchs but did not check the boolean return value. The compiler emitted CALL followed by POP, discarding the success flag.

Details: King of the Ether Throne was an early Ethereum game where players paid increasing amounts of ETH to claim the “throne.” When a new monarch took over, the contract sent the claim price to the previous monarch via .send(). The Mist Ethereum Wallet created contract-based wallets for users, and these contract wallets required more than the 2300 gas stipend that .send() provides. When sending ETH to these contract wallets, the .send() call failed and returned false, but the contract executed POP on that false value and continued as if payment succeeded.

The contract marked the compensation as “sent” in its internal accounting regardless of the transfer outcome. The ETH remained locked in the King of the Ether contract, and dethroned monarchs never received their compensation. The contract had no withdrawal mechanism for failed payments.

POP’s role: At the bytecode level, the vulnerability is a CALL (which pushes 0 for failure) immediately followed by POP (which discards that 0). Without a conditional jump checking the success flag between CALL and POP, the failure is invisible to the contract’s logic.

Impact: Multiple users lost ETH compensation payments. While the total losses were modest by modern standards (< 100 ETH), the exploit became the canonical example of the unchecked return value vulnerability and directly influenced the design of Solidity’s transfer() function, which auto-reverts on failure.

References:


Exploit 2: Aperture Finance — Unchecked Low-Level Call Enabling $3.67M Drain (January 2026)

Root cause: Insufficient input validation on parameters passed to low-level .call(), combined with failure to check return values, allowed attackers to redirect execution to arbitrary contracts.

Details: On January 25, 2026, Aperture Finance was exploited across Ethereum, Arbitrum, and Base chains for over $3.67 million. The attacker exploited an arbitrary-call vulnerability where user-controlled parameters were passed directly into low-level call() invocations without proper validation or return value checking. The attacker crafted call parameters that targeted ERC-20 token contracts, invoking transferFrom() to drain assets by abusing existing token approvals that users had granted to the Aperture contracts.

The core issue was twofold: (1) the contract did not validate the target address or calldata of the low-level call, and (2) the return value of the call was not checked, so even if the call had partially failed or returned unexpected data, execution would have continued.

POP’s role: The unchecked call pattern at the bytecode level follows the same CALL ... POP structure. The success flag was discarded, and the contract could not distinguish between a successful token transfer and a failed one. While the primary vulnerability was the arbitrary call target (not the unchecked return), the POP of the success flag eliminated a potential defense layer.

Impact: $3.67M drained across three chains. The exploit demonstrated that unchecked low-level calls remain a live vulnerability class in production DeFi, even in 2026.

References:


Exploit 3: Unchecked send() in Early DeFi — Recurring Pattern (2016-2019)

Root cause: Widespread use of .send() without return value checking across early Ethereum contracts, compiling to the CALL POP bytecode pattern.

Details: Between 2016 and 2019, dozens of contracts used the pattern recipient.send(amount) (or the even earlier recipient.call.value(amount)()) without checking the return value. The SWC (Smart Contract Weakness Classification) registry cataloged this as SWC-104, and empirical studies found the pattern in thousands of deployed contracts. Common failure modes included:

  • Lottery and gambling contracts that marked winners as “paid” regardless of whether the ETH transfer succeeded. Failed payments left ETH stuck in the contract with no withdrawal path.
  • Multi-party payment splitters that iterated over a list of recipients, calling .send() in a loop without checking returns. One failed send (e.g., to a contract wallet) silently skipped that recipient while continuing to pay others.
  • Crowdfunding contracts where refund mechanisms used unchecked .send(). When refunds failed (recipient reverted), the contract marked refunds as complete, permanently locking contributor funds.

The Solidity team introduced .transfer() (which auto-reverts on failure) specifically to combat this pattern. Later, the community shifted to the checks-effects-interactions pattern with explicit return value checking via require(success) after .call{value: amount}("").

POP’s role: In every instance, the compiled bytecode shows the same structure: CALL pushes a success flag, then POP immediately discards it. The POP instruction is the bytecode-level mechanism that makes the vulnerability possible — it is the point where the contract’s ability to detect failure is permanently destroyed.

Impact: Cumulative losses across many contracts totaling hundreds to thousands of ETH. More importantly, the pattern established the unchecked return value as one of the “classic” smart contract vulnerability classes, leading to SWC-104, static analyzer rules in Slither/Mythril/Securify, and the OWASP Smart Contract Top 10 (SC06).

References:


Attack Scenarios

Scenario A: Unchecked send() in a Payment Contract

contract VulnerablePayment {
    mapping(address => uint256) public balances;
    mapping(address => bool) public paid;
 
    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }
 
    function payOut(address payable recipient) external {
        require(!paid[recipient], "already paid");
        uint256 amount = balances[recipient];
        require(amount > 0, "no balance");
 
        // VULNERABLE: .send() returns false if recipient reverts,
        // but the return value is POPped by the compiler.
        // The contract marks payment as complete regardless.
        recipient.send(amount);
 
        // State updated even if send() failed -- funds are stuck
        paid[recipient] = true;
        balances[recipient] = 0;
    }
 
    // Attack: Recipient is a contract whose receive() reverts.
    // .send() returns false, POP discards it, paid[recipient] = true.
    // The ETH stays in this contract, and recipient can never claim it.
    // If a second mechanism relies on paid[recipient] == true,
    // the attacker can exploit the inconsistent state.
}

Scenario B: Assembly pop(call(…)) Hiding a Failed Transfer

contract VulnerableDEXRouter {
    function swapAndForward(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        address recipient
    ) external {
        // Perform swap (simplified)
        uint256 amountOut = _executeSwap(tokenIn, tokenOut, amountIn);
 
        // VULNERABLE: Assembly pop discards transfer success flag.
        // If the ERC-20 transfer fails (e.g., recipient is blacklisted),
        // the router believes tokens were forwarded.
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
            mstore(add(ptr, 4), recipient)
            mstore(add(ptr, 36), amountOut)
            // pop discards the success boolean -- transfer failure is invisible
            pop(call(gas(), tokenOut, 0, ptr, 68, 0, 0))
        }
 
        // Contract proceeds as if recipient received tokens.
        // Tokens remain in the router; attacker can claim them later.
        emit SwapCompleted(tokenIn, tokenOut, amountIn, amountOut, recipient);
    }
 
    function _executeSwap(address, address, uint256) internal pure returns (uint256) {
        return 1000; // simplified
    }
    event SwapCompleted(address, address, uint256, uint256, address);
}

Scenario C: Stack Underflow DoS via Malformed Bytecode

// Hand-crafted bytecode that underflows on a specific path
// 
// Normal path:  PUSH1 0x01 PUSH1 0x02 ADD POP STOP
//               Stack: [1] -> [1,2] -> [3] -> [] -> halt
//
// Malicious path: JUMPDEST POP STOP
//                 Stack: [] -> EXCEPTION (underflow)
//
// If an attacker can force a JUMP to the JUMPDEST at the start
// of the malicious path when the stack is empty, POP triggers
// a stack underflow exception. The transaction reverts, consuming
// all gas forwarded to this context.
//
// In a real contract, this manifests when a rarely-tested code path
// (e.g., error handling branch) has a stack depth mismatch.
// The contract works for normal operations but reverts for edge cases,
// potentially blocking withdrawals or liquidations.

Scenario D: Discarded CREATE2 Return Value

contract VulnerableFactory {
    function deployAndFund(bytes32 salt, bytes memory bytecode) external payable {
        address deployed;
        assembly {
            deployed := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        }
 
        // VULNERABLE: Does not check if deployed == address(0)
        // (deployment failure). If create2 fails (e.g., contract already
        // exists at that address, or init code reverts), deployed is 0.
        // The ETH is sent to address(0), which burns it permanently.
        (bool success,) = deployed.call{value: msg.value}("");
        require(success, "funding failed");
 
        // Even if we checked success here, sending ETH to address(0)
        // succeeds (it's a valid transfer to the zero address).
        // The real bug is discarding the create2 result without
        // checking for zero -- conceptually the same as POP on a
        // critical return value.
    }
}

Mitigations

ThreatMitigationImplementation
T1: Unchecked external call returnAlways check the boolean return value of low-level calls(bool success,) = target.call{value: amount}(""); require(success, "call failed");
T1: Unchecked send()Use transfer() (auto-reverts) or checked call()Replace recipient.send(amount) with (bool ok,) = recipient.call{value: amount}(""); require(ok);
T1: Non-standard ERC-20 returnsUse OpenZeppelin’s SafeERC20 librarySafeERC20.safeTransfer(token, recipient, amount) handles tokens that return nothing, false, or revert
T2: Stack underflow DoSUse high-level Solidity instead of raw bytecode; test all code pathsComprehensive test coverage including error/edge paths; formal verification for critical contracts
T2: EOF stack validationDeploy as EOF-formatted contracts when availableEOF (EIP-5450) catches stack underflow at deploy time; no runtime risk for validated contracts
T3: Compiler optimization discarding valuesPin compiler versions; audit compiled bytecode for critical contractsCompare source-level intent against emitted bytecode; use forge inspect to review compiler output
T4: Assembly pop misuseAvoid assembly for external calls; when unavoidable, always check successReplace pop(call(...)) with let ok := call(...) if iszero(ok) { revert(0, 0) }
T4: Static analysis blind spotsRun Slither, Mythril, or Securify on compiled bytecodeSlither’s unchecked-lowlevel and unchecked-send detectors specifically flag CALL POP patterns
T5: Discarded CREATE/CREATE2 returnAlways check for address(0) after deploymentrequire(deployed != address(0), "deployment failed")
General: Withdrawal patternUse pull-over-push for ETH distributionInstead of pushing ETH to recipients (where send can fail), let recipients withdraw via a claim() function

Compiler/EIP-Based Protections

  • Solidity transfer() (>= 0.4.13): Introduced as a safe alternative to .send(). transfer() automatically reverts on failure, eliminating the CALL POP pattern for ETH transfers. Limited to 2300 gas, which prevents reentrancy but can fail for contract recipients that need more gas.
  • Solidity >= 0.8.0 compiler warnings: The compiler emits a warning when the return value of .send() or low-level .call() is not used, directly flagging the pattern that compiles to CALL POP.
  • SafeERC20 (OpenZeppelin): Wraps ERC-20 transfer, transferFrom, and approve calls with return value checking and handles non-compliant tokens (no return value, false return). Eliminates the POP-based discard of token transfer results.
  • EIP-5450 (EOF Stack Validation): Validates stack depth at deploy time for EOF contracts. POP on an empty stack prevents contract deployment, eliminating runtime stack underflow entirely.
  • Slither unchecked-lowlevel / unchecked-send detectors: Static analysis rules that detect the CALL ... POP pattern in bytecode and flag unchecked return values at the source level.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalHighKing of Ether Throne (2016), Aperture Finance ($3.67M, 2026), SWC-104 in thousands of contracts
T2Smart ContractMediumLowNo major exploit solely from POP underflow; mitigated by compiler-generated stack checks
T3Smart ContractMediumLowSolidity optimizer bugs (2022); no direct exploit from POP-related optimization
T4Smart ContractHighMediumRecurring pattern in MEV bots and DEX routers using assembly; Aperture Finance
T5Smart ContractLowLowContract deployment failures from discarded CREATE return values; funds locked but no major named exploit
P1ProtocolLowN/A
P2ProtocolLowN/A
P3ProtocolLowN/AEIP-5450 pending; eliminates T2 for EOF contracts

OpcodeRelationship
ISZERO (0x15)The safe alternative to POP after a CALL. ISZERO checks whether the CALL’s return value is 0 (failure) and is used with JUMPI to branch on call failure. The pattern CALL ISZERO PUSH jumpdest JUMPI is the checked-call equivalent of the vulnerable CALL POP.
CALL (0xF1)Pushes a success flag (0 or 1) onto the stack. When followed by POP instead of a conditional check, this creates the unchecked external call vulnerability (SWC-104). CALL is the primary opcode whose return value POP dangerously discards.
DUP1 (0x80)Duplicates the top stack item. Used to preserve a value before POP consumes it. The pattern CALL DUP1 ... POP allows checking the call result while still cleaning up the stack. DUP1 before POP is a defensive pattern to retain values that might be needed.
DELEGATECALL (0xF4)Like CALL, pushes a success flag. Unchecked DELEGATECALL (followed by POP) is especially dangerous because a failed delegatecall can leave the calling contract in an inconsistent state with its storage partially modified.
STATICCALL (0xFA)Read-only external call that pushes a success flag. Discarding this via POP means the contract cannot distinguish between “the call returned valid data” and “the call reverted.”
CREATE (0xF0) / CREATE2 (0xF5)Push the deployed contract address (or 0 on failure). POP discarding this value means the contract cannot verify deployment succeeded, potentially sending funds to address(0).
SWAP1 (0x90)Swaps top two stack items. Used in conjunction with POP to discard a specific stack item that is not on top. SWAP1 POP discards the second-from-top item.
JUMPI (0x57)Conditional jump that consumes the top stack item as the condition. The safe pattern after CALL is ISZERO PUSH dest JUMPI (jump to error handler if call failed). Replacing this with POP eliminates the safety branch entirely.