Opcode Summary

PropertyValue
Opcode0x00
MnemonicSTOP
Gas0
Stack Input(none)
Stack Output(none)
BehaviorHalts execution successfully. No return data. No state revert.

Threat Surface

STOP is the simplest opcode, but its security significance is disproportionately high. It is the implicit opcode of empty code — when the EVM encounters no bytecode (e.g., an EOA, a destroyed contract, or uninitialized storage), execution is equivalent to hitting STOP. This means:

  • Any call to an address with no code succeeds silently instead of reverting.
  • 0x00 is the zero byte, meaning uninitialized bytecode defaults to STOP.
  • Fallback functions in older Solidity contracts can turn undefined function calls into no-ops that succeed via STOP.

This “silent success” property is the root cause of multiple billion-dollar vulnerabilities.


Smart Contract Threats

T1: Delegatecall to Empty/Destroyed Contract (Critical)

When a proxy contract uses delegatecall to forward execution to an implementation contract, and that implementation is destroyed (via SELFDESTRUCT) or was never deployed, the delegatecall targets address with no code. The EVM executes STOP, returning success. The proxy now silently does nothing for every call, permanently bricking itself.

Why it matters: Proxy patterns (UUPS, Transparent, Diamond) are the backbone of upgradeable contracts. A destroyed or uninitialized implementation means the proxy accepts every call and does nothing — funds become permanently locked.

T2: Phantom Functions / Fallback No-ops (Critical)

Contracts with a non-reverting fallback function (common in pre-Solidity 0.6 code) accept calls to any function selector, even ones they don’t implement. The call matches no function, falls through to the fallback (or just hits STOP if the fallback is empty), and returns success. The caller assumes the function executed correctly.

Why it matters: When contract A calls token.permit(...) on a token that doesn’t implement permit but has a fallback, the call succeeds as a no-op. Contract A then proceeds as if the permit was granted, potentially allowing theft of approved funds.

T3: Calls to EOAs / Empty Addresses (High)

Low-level calls (.call(), .delegatecall()) to externally owned accounts (EOAs) or non-existent addresses return success = true because the EVM hits STOP immediately. If the caller doesn’t verify the target has code, it treats the no-op as a successful operation.

Why it matters: Bridge contracts, token wrappers, and any contract that calls arbitrary addresses via low-level calls are vulnerable. The caller’s control flow assumes something happened when nothing did.

T4: Missing/Silent Fallback in Receiver Contracts (Medium)

A contract meant to receive tokens or ETH may have an empty fallback/receive function. While this “works” for receiving ETH, it also silently accepts any misrouted function call without reverting, making bugs harder to detect.


Protocol-Level Threats

P1: No Direct DoS Vector (Low)

STOP costs 0 gas and halts immediately. It cannot be used for gas griefing or computational DoS. It is the cheapest possible execution path.

P2: Consensus Divergence Risk (Low)

STOP is trivially simple with no edge cases in its own behavior. All client implementations agree on its semantics. No known consensus bugs have arisen from STOP itself.

P3: State Trie Impact (None)

STOP makes no state changes. It cannot cause state bloat.

P4: Implicit Behavior as Default Byte (Medium)

0x00 is the zero byte, so it appears in:

  • Uninitialized contract code regions
  • Zero-padded memory and calldata
  • Bytecode that was incorrectly constructed

Any EVM client implementation must correctly handle the case where execution jumps to or falls through to a region of zero bytes, treating each 0x00 as STOP rather than an invalid opcode.


Edge Cases

Edge CaseBehaviorSecurity Implication
Call to address with no codeReturns success, returndatasize = 0Silent success; caller may interpret as valid response
Delegatecall to address with no codeReturns success, no state changesProxy is bricked; all calls are no-ops
STOP in middle of PUSHn dataNot executed as STOP (it’s data, not code)Only relevant if JUMPDEST analysis is incorrect
First byte of uninitialized codeExecutes as STOPEmpty contracts “succeed” on any call
STOP after state-changing opcodesState changes persistPartial execution before STOP is committed
Returndatasize after STOPReturns 0Callers checking return data get nothing

Real-World Exploits

Exploit 1: Parity Wallet Freeze — $150M Frozen (November 2017)

Root cause: Delegatecall to a destroyed library contract, which then returned STOP (success).

Details: Parity’s multi-sig wallets used a shared library contract via delegatecall. The library itself was left uninitialized (m_numOwners == 0). An attacker called initWallet() directly on the library, became its owner, then called selfdestruct on it. With the library destroyed, all 587 wallet contracts were bricked — every delegatecall now targeted empty code (STOP), silently succeeding while doing nothing. 513,774 ETH (~$150M) became permanently inaccessible.

STOP’s role: After the library was destroyed, every delegatecall from the wallet proxies hit STOP. The calls returned success, but no wallet logic executed. The funds could never be moved again.

References:


Exploit 2: Multichain “Phantom Functions” — $1B+ at Risk (January 2022)

Root cause: Calling permit() on WETH, which doesn’t implement permit but has a non-reverting fallback that effectively executes STOP-equivalent behavior (a no-op).

Details: Multichain’s depositWithPermit() function called IERC20(underlying).permit(target, ...) before transferring tokens. For WETH, permit is undefined, but WETH has an old-style function() public payable fallback that calls deposit(). Since no ETH was sent, the fallback’s deposit() was a no-op — functionally equivalent to STOP. The contract then called safeTransferFrom(target, ...), which succeeded because users had already approved the contract via the normal deposit() path. An attacker could steal any user’s approved WETH.

STOP’s role: The phantom permit() call silently succeeded (no revert), bypassing the authorization check. The contract assumed the permit was valid and proceeded to transfer the victim’s tokens.

Impact: 1B total across chains. ~0.5% of exposed funds were actually stolen before mitigation.

References:


Exploit 3: Qubit Bridge Hack — $80M Stolen (January 2022)

Root cause: Calling safeTransferFrom on the zero address (no code), which returned success via STOP.

Details: Qubit Finance’s Ethereum-BSC bridge had a deposit() function that called a custom safeTransferFrom() using low-level .call() instead of OpenZeppelin’s SafeERC20. When the attacker passed address(0) as the token address, the .call() targeted an address with no code. The EVM hit STOP, returning success. The bridge interpreted this as a valid deposit and minted qXETH on BSC. The attacker repeated this to accumulate tokens worth $80M, then drained the BSC-side liquidity pools.

STOP’s role: The call to the zero address succeeded because there’s no code there — execution immediately hits STOP. The bridge’s lack of code-existence checks meant this silent success was treated as a real token transfer.

References:


Exploit 4: OpenZeppelin UUPS Proxy Vulnerability — $50M+ at Risk (September 2021)

Root cause: Uninitialized UUPS implementation contracts could be taken over, self-destructed, leaving the proxy delegating to empty code (STOP).

Details: UUPS proxies store upgrade logic in the implementation contract itself. If the implementation was never initialized, an attacker could call the initializer, become the admin, then upgrade the implementation to a contract containing selfdestruct. After destruction, the proxy’s delegatecall targets empty code, hitting STOP on every call. The proxy is permanently bricked with no recovery path, since the upgrade mechanism was in the now-destroyed implementation.

STOP’s role: Post-destruction, the proxy silently succeeds on every call (STOP), but no logic executes. All funds and state in the proxy are permanently locked.

References:


Attack Scenarios

Scenario A: Proxy Brick via Implementation Destruction

sequenceDiagram
    participant A as Attacker
    participant P as Proxy
    participant I as Implementation

    A->>P: initialize
    P->>I: delegatecall
    Note over I: attacker becomes owner

    A->>P: upgradeTo evil
    P->>I: delegatecall
    Note over I: selfdestructs, code empty

    A->>P: transfer
    P->>I: delegatecall
    Note over I: STOP, funds locked

Scenario B: Phantom Function Token Drain

// Vulnerable router contract
function depositWithPermit(address token, address victim, uint256 amount, ...) external {
    // If token has no permit() but has a fallback, this is a no-op (STOP)
    IERC20(token).permit(victim, address(this), amount, ...);
    
    // This succeeds if victim previously approved this contract
    IERC20(token).safeTransferFrom(victim, address(this), amount);
    
    _mint(msg.sender, amount);
}

Scenario C: Bridge Deposit Forgery

// Vulnerable bridge contract
function deposit(address token, uint256 amount) external {
    // Low-level call to address(0) or EOA returns success (STOP)
    (bool success, ) = token.call(
        abi.encodeWithSelector(IERC20.transferFrom.selector, msg.sender, address(this), amount)
    );
    require(success); // Passes! No code = STOP = success
    
    emit Deposit(msg.sender, token, amount); // Bridge mints on other chain
}

Mitigations

ThreatMitigationImplementation
T1: Delegatecall to empty contractAlways initialize implementation contracts immediately upon deploymentUse OpenZeppelin’s _disableInitializers() in constructor
T1: Proxy brickStore upgrade logic in the proxy, not the implementationUse Transparent Proxy pattern or add recovery mechanism
T2: Phantom functionsNever assume external function calls revert on failureCheck return data length; use try/catch
T2: Phantom permitVerify target contract actually implements permit via EXTCODESIZE + interface checkUse OpenZeppelin’s SafeERC20.safePermit()
T3: Calls to EOAsCheck that target has code before low-level callsrequire(addr.code.length > 0) or use Address.functionCall()
T3: Silent successUse Solidity’s high-level call syntax (reverts on no-code) or OpenZeppelin SafeERC20Replace .call() with direct interface calls where possible
GeneralTreat success = true from low-level calls as necessary but not sufficientAlways validate return data and/or check code existence

Compiler/EIP-Based Protections

  • Solidity >= 0.8.0: High-level external calls (IERC20(addr).transfer(...)) revert if the target has no code. This mitigates T3 at the language level.
  • EIP-3541: Rejects deployment of contracts starting with 0xEF byte, preventing certain code confusion attacks.
  • OpenZeppelin Initializable: Provides _disableInitializers() to prevent uninitialized implementation takeover (mitigates T1).

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalMediumParity (50M+)
T2Smart ContractCriticalMediumMultichain ($1B at risk)
T3Smart ContractHighHighQubit ($80M)
T4Smart ContractMediumMedium
P1ProtocolLowN/A
P2ProtocolLowLow
P4ProtocolMediumLow

OpcodeRelationship
SELFDESTRUCT (0xFF)Destroys contract code, leaving future calls to hit STOP
DELEGATECALL (0xF4)Primary vector for proxy-to-empty-code exploits
CALL (0xF1)Low-level calls to codeless addresses return STOP-success
RETURN (0xF3)Unlike STOP, RETURN provides return data; their difference matters for callers
REVERT (0xFD)The explicit failure path that STOP is not — their contrast is the root of silent success bugs
EXTCODESIZE (0x3B)Used to check if target has code before calling (mitigation for T3)