Opcode Summary

PropertyValue
Opcode0xFE
MnemonicINVALID
GasConsumes ALL remaining gas
Stack InputN/A
Stack OutputN/A
BehaviorDesignated invalid instruction (EIP-141). Immediately halts execution with an exceptional abort, consuming all remaining gas in the current execution context. No state changes are persisted. All undefined/unassigned opcodes (e.g., 0xA5-0xEE, 0xEF, 0xF6-0xF9, 0xFB, 0xFC) behave identically — triggering an exceptional halt that burns all remaining gas. The Solidity compiler emits 0xFE for assert() failures (pre-0.8.0), division by zero, array out-of-bounds access, and as bytecode padding after the runtime code to guard against execution falling off the end of valid code.

Threat Surface

INVALID is unique among EVM opcodes because its sole purpose is to destroy gas. Unlike REVERT (0xFD), which returns unused gas to the caller, INVALID consumes every unit of gas remaining in the current call frame. This makes it both a safety mechanism (signaling that something has gone catastrophically wrong) and a potential weapon for griefing attacks.

The threat surface centers on four properties:

  1. All-gas consumption is the defining behavior. When INVALID executes, gasRemaining is set to zero. The entire gas allocation for the current call frame is burned. In a top-level transaction, this means the full gas limit the sender specified is consumed. In a subcall (CALL, DELEGATECALL, STATICCALL), the 63/64 of gas forwarded to the subcall is destroyed, but the parent frame retains its 1/64 reserve. This gas destruction is permanent — there is no refund mechanism, no error data returned, and no way to recover the gas. The distinction from REVERT is critical: a REVERT with 10M gas remaining returns ~10M gas to the caller, while INVALID with 10M gas remaining destroys all of it.

  2. Solidity uses INVALID for critical invariant violations. Before Solidity 0.8.0, the compiler emitted the INVALID opcode (0xFE) for assert() failures, arithmetic overflow/underflow, division by zero, array out-of-bounds access, enum conversion errors, and calling uninitialized internal function pointers. The design intent was that hitting INVALID means a bug exists in the contract — a condition the developer believed was impossible has occurred. Post-0.8.0, these conditions emit REVERT with Panic(uint256) error codes instead, preserving unused gas. However, millions of pre-0.8.0 contracts remain deployed on mainnet with INVALID-based assertions that will burn all gas on failure.

  3. INVALID is the gas-burning primitive for block stuffing attacks. The Fomo3D exploit (2018) demonstrated that assert(false) is an efficient way to consume an entire block’s gas limit. An attacker submits transactions with high gas limits that deliberately hit INVALID, filling blocks with gas-burning no-ops. This prevents legitimate transactions from being included, enabling time-sensitive exploits against countdown-based contracts. The gas cost to the attacker is real (they pay for the consumed gas), but the attack is economically viable when the prize exceeds the gas cost.

  4. Undefined opcodes are silent INVALID equivalents. Any opcode not assigned by the EVM specification triggers the same exceptional halt as 0xFE. This includes the entire 0xA5-0xEE range (excluding 0xA0-0xA4 LOG opcodes), 0xF6-0xF9, 0xFB, and 0xFC. Contracts containing these bytes as executable code (not as PUSH data) will silently burn all gas if execution reaches them. This is a forward-compatibility hazard: if a future hard fork assigns behavior to a previously undefined opcode, deployed contracts containing that opcode as “dead code” could suddenly execute new logic.


Smart Contract Threats

T1: Gas Griefing via Forced INVALID Execution (High)

An attacker who can force a contract to execute the INVALID opcode destroys all gas allocated to that call, imposing maximum cost on the transaction sender. This is exploitable in multiple ways:

  • Triggering assert() failures in external contracts. If a contract’s public function contains an assert() that depends on internal state, an attacker who can manipulate that state (e.g., via a preceding call in the same transaction) can force the assertion to fail. In pre-0.8.0 contracts, this burns all forwarded gas. The attacker does not profit directly but can grief other users or protocols that interact with the contract.

  • Gas-burning in subcalls with high gas forwarding. When contract A calls contract B with B.call{gas: gasleft()}(...) and contract B hits INVALID, 63/64 of A’s remaining gas is destroyed. If A does not handle the failure gracefully (e.g., no try/catch, no gas limit on the subcall), the entire transaction’s gas budget is effectively wasted. This is especially dangerous in multi-hop call chains (aggregators, routers, batch executors) where a single INVALID in a leaf call destroys gas for the entire chain.

  • Block stuffing via deliberate INVALID. An attacker submits transactions with the maximum gas limit (30M) that immediately execute assert(false) or jump to an undefined opcode. Each transaction consumes 30M gas, filling the block. At ~$5-50 per block (depending on gas prices), this can block legitimate transactions for the duration of the attack. The Fomo3D exploit used this technique across 14 consecutive blocks.

Why it matters: Gas griefing via INVALID is cheap relative to the damage it inflicts. A single transaction can destroy 30M gas worth of value, and the attack requires no special privileges — only ETH to pay for gas.

T2: assert() vs require() Confusion — INVALID vs REVERT (High)

Misusing assert() where require() should be used exposes contracts to unnecessary all-gas consumption on failure:

  • Input validation with assert(). Developers who use assert(amount > 0) instead of require(amount > 0) cause users who submit invalid inputs to lose their entire gas allocation. In pre-0.8.0 contracts, this is a direct DoS vector: an attacker can submit transactions with high gas limits and invalid inputs, knowing the gas will be completely destroyed rather than refunded.

  • Pre-0.8.0 contracts still deployed. The Solidity compiler change in 0.8.0 (replacing INVALID with REVERT + Panic codes) does not retroactively fix deployed contracts. Every pre-0.8.0 contract using assert() for any check that can be triggered by external input remains vulnerable to all-gas destruction. This includes major DeFi protocols deployed in 2019-2020 that have not been upgraded.

  • Mixed compiler versions in protocol stacks. A protocol may have newer contracts (0.8.x, using REVERT) calling older contracts (0.6.x or 0.7.x, using INVALID). A failure in the older contract destroys all forwarded gas, surprising the newer contract’s gas accounting logic. Error handling that works correctly with REVERT-based failures may behave unexpectedly when the subcall hits INVALID instead.

Why it matters: The assert/require distinction is one of the most documented Solidity pitfalls (SWC-110, OWASP SCWE-067), yet pre-0.8.0 contracts with this issue remain deployed and actively used across DeFi.

T3: Undefined Opcodes in Deployed Bytecode (Medium)

Contracts containing undefined opcode bytes in executable code paths face both immediate and future risks:

  • Immediate: all-gas destruction on execution. If a contract’s bytecode contains an undefined opcode (e.g., 0xB0) in a reachable code path — due to a compiler bug, hand-written assembly, or bytecode manipulation — any transaction that executes that path will burn all remaining gas and revert.

  • Forward compatibility hazard. If a future Ethereum hard fork assigns behavior to a previously undefined opcode, deployed contracts containing that opcode could change behavior. A byte that previously caused an immediate halt might now execute new logic, potentially altering the contract’s semantics. EIP-3670 (EOF Code Validation) addresses this for EOF-formatted contracts by rejecting undefined opcodes at deploy time, but legacy contracts are unaffected.

  • Bytecode injection via CODECOPY/EXTCODECOPY. If a contract copies bytecode from an external source and executes it (e.g., via CREATE/CREATE2 with runtime code copied from another contract), undefined opcodes in the source can silently burn gas when the deployed code is called.

Why it matters: The EVM’s permissive deployment model allows any bytes to be stored as contract code. Without deploy-time validation (which only EOF provides), undefined opcodes are ticking time bombs.

T4: Compiler-Generated INVALID as Unreachable Code Guard (Low)

The Solidity compiler strategically places INVALID opcodes as guards in generated bytecode:

  • End-of-code padding. The compiler appends 0xFE after the contract’s runtime bytecode (before the metadata hash) to prevent execution from falling off the end of valid code into the constructor code or metadata. If a bug causes the program counter to overrun, it hits INVALID rather than executing arbitrary bytes.

  • Division-by-zero and array-bounds guards (pre-0.8.0). Before 0.8.0, the compiler emitted INVALID for a / b when b == 0 and for array[i] when i >= array.length. While this correctly prevents undefined behavior, the all-gas penalty is disproportionate for what may be a user-input error rather than an invariant violation.

  • Switch/jump table fallthrough. In complex function dispatch logic (especially with many external functions), the compiler may emit INVALID as the default case when no function selector matches. Combined with the function signature check at the contract’s entry point, this ensures unknown function calls fail explicitly.

Why it matters: These are protective measures, not vulnerabilities per se. But developers should understand that hitting compiler-generated INVALID in production means either a compiler bug, a state inconsistency, or (in pre-0.8.0) an ordinary input validation failure that is being punished with all-gas destruction.

T5: INVALID in Cross-Contract Calls — 63/64 Gas Destruction (Medium)

When INVALID executes inside a subcall, it interacts with the EVM’s 63/64 gas forwarding rule (EIP-150) to create counterintuitive gas destruction patterns:

  • 63/64 of forwarded gas is destroyed. When contract A calls contract B, A forwards at most 63/64 of its remaining gas. If B hits INVALID, that entire 63/64 is burned. A retains only its 1/64 reserve, which may not be enough to complete its own execution, causing A to also revert (but via out-of-gas, not INVALID, so A’s behavior depends on how it handles the failed subcall).

  • try/catch does not prevent gas loss. Solidity’s try/catch catches the revert from a subcall that hit INVALID, but the gas is already destroyed. The catch block executes with only the 1/64 reserve remaining, which may be insufficient for meaningful error handling or state cleanup.

  • Cascading gas destruction in deep call stacks. In a chain A B C D, if D hits INVALID, it destroys 63/64 of C’s forwarded gas. C then likely runs out of gas too (its 1/64 reserve is insufficient), destroying 63/64 of B’s forwarded gas, and so on up the chain. A single INVALID at the bottom of a deep call stack can cascade gas destruction through the entire stack.

Why it matters: Multi-hop call chains are pervasive in DeFi (DEX aggregators, liquidation bots, flash loan callbacks). A single pre-0.8.0 contract in the chain that hits an assert failure can destroy gas for every contract above it in the call stack.


Protocol-Level Threats

P1: Undefined Opcode Behavior and Forward Compatibility (Medium)

The EVM specification states that executing an undefined opcode causes an exceptional halt identical to INVALID. As of 2026, the unassigned opcode space includes 0xA5-0xEE (excluding LOG0-LOG4 at 0xA0-0xA4), 0xEF (reserved by EIP-3541 for EOF), and several gaps in the 0xF0-0xFF range. These bytes are treated as INVALID at the protocol level, but they are not guaranteed to remain INVALID forever.

Security implications:

  • Hard fork behavior changes. If a future hard fork assigns a non-halting behavior to an opcode that was previously undefined, any deployed contract containing that opcode byte in executable code will suddenly have new logic injected into its execution path. This is a theoretical but real risk that motivates the EOF initiative (EIP-3540, EIP-3670), which validates opcodes at deploy time.

  • EIP-3541 and the 0xEF reservation. Since the London hard fork (August 2021), new contracts cannot be deployed with bytecode starting with 0xEF. This was done to reserve the prefix for EOF, ensuring that no legacy contracts could be confused with EOF-formatted contracts. The 0xEF byte itself still causes an exceptional halt if executed, but existing contracts deployed before London that start with 0xEF (none were found on mainnet) would be unaffected.

  • Client implementation divergence risk. All EVM clients must agree on which opcodes are valid and which are INVALID. If a client accidentally assigns behavior to an undefined opcode, it would produce different execution results, causing a consensus split. This has not happened in practice, but the large undefined opcode space increases the surface area for implementation bugs.

P2: Gas Accounting and INVALID in EIP-4337 Account Abstraction (Low)

In EIP-4337 account abstraction, the EntryPoint contract executes user operations with precise gas accounting. If the user operation’s callData execution hits INVALID (e.g., calling a pre-0.8.0 contract with an assert failure), the gas destruction affects the paymaster’s gas budget:

  • Paymaster overcharging. The paymaster pre-pays gas for the user operation. If INVALID destroys all forwarded gas, the paymaster is charged for the full gas amount even though no useful work was done. The postOp gas stipend can exacerbate this — a paymaster may be charged up to 959% more than expected if INVALID triggers during execution and the postOp accounting does not handle the all-gas-consumed case correctly.

  • Bundler incentive misalignment. Bundlers (entities that submit user operations to the mempool) simulate transactions before inclusion. A transaction that passes simulation but hits INVALID on-chain (due to state changes between simulation and execution) wastes the bundler’s gas allocation and may not compensate them adequately.


Edge Cases

Edge CaseBehaviorSecurity Implication
INVALID (0xFE) vs REVERT (0xFD) gas behaviorINVALID consumes ALL remaining gas; REVERT refunds unused gas and can return error dataContracts using assert() pre-0.8.0 impose maximum gas cost on failure. Post-0.8.0, assert() uses REVERT with Panic(uint256) codes, preserving gas.
Undefined opcodes (0xA5-0xEE, 0xF6-0xF9, etc.)Behave identically to INVALID: exceptional halt, all gas consumedForward-compatibility risk if future hard forks assign behavior; immediate risk of all-gas destruction if execution reaches them.
0xEF opcode (reserved by EIP-3541)Exceptional halt like INVALID; additionally, new contracts cannot be deployed with bytecode starting with 0xEF (since London)Reserved for EOF. Cannot be accidentally deployed in new contracts; existing contracts with 0xEF in non-starting positions still risk all-gas destruction.
Solidity assert() pre-0.8.0Compiles to INVALID (0xFE); burns all gas on failureSignificant gas griefing surface on legacy contracts. Millions of pre-0.8.0 contracts remain deployed.
Solidity assert() post-0.8.0Compiles to REVERT with Panic(0x00) error code; preserves unused gasSafe from gas destruction. Can be caught by try/catch with meaningful gas remaining.
Solidity panic codes (0.8.0+)Division by zero = Panic(0x12), array OOB = Panic(0x32), overflow = Panic(0x11)All use REVERT, not INVALID. Legacy contracts still use INVALID for these conditions.
INVALID in constructor (initcode)All gas consumed; contract deployment fails; no code is storedDeployment transaction pays full gas. No contract exists at the CREATE/CREATE2 address.
INVALID inside STATICCALLExceptional halt, all forwarded gas consumed, returns 0 (failure) on the stackSame gas destruction as in regular CALL. The read-only restriction does not change INVALID’s gas behavior.
INVALID inside DELEGATECALLExceptional halt in the delegate context; all forwarded gas consumed; caller’s storage is unmodified (changes rolled back)State is safe (rolled back), but gas is destroyed. The delegating contract’s 1/64 reserve may be insufficient to continue.
0xFE as PUSH data (not executed)If 0xFE appears as an operand to a PUSH instruction, it is treated as data, not executedNo security impact. Only executed opcodes matter; PUSH operands are skipped during JUMPDEST analysis.
INVALID with 0 gas remainingNo additional effect; execution was already about to halt from out-of-gasNo practical difference from a regular out-of-gas error in this edge case.

Real-World Exploits

Exploit 1: Fomo3D Block Stuffing — assert(false) to Burn Block Gas and Win $3M Jackpot (August 2018)

Root cause: The Fomo3D game’s countdown timer could be manipulated by preventing other players’ transactions from being included in blocks. The attacker used assert() failures (INVALID opcode) as the gas-burning mechanism to fill blocks cheaply.

Details: Fomo3D was a “last player wins” game where a 24-hour countdown timer reset with each key purchase. The attacker’s strategy was to buy the last key and then prevent anyone else from buying keys until the timer expired. To accomplish this, the attacker deployed smart contracts that:

  1. Called getCurrentRoundInfo() on the Fomo3D contract to check the game state (remaining time, current round leader).
  2. If conditions were favorable (attacker was the last key buyer and the timer was about to expire), the contract executed assert(false), which compiled to the INVALID opcode (0xFE).
  3. These INVALID-triggering transactions were submitted with high gas limits and high gas prices, ensuring miners prioritized them.

The assert(false) pattern was specifically chosen because it consumes ALL remaining gas — the most efficient way to fill a block’s gas capacity. Each attack transaction consumed its full gas allocation (up to 4.2M gas), and the attacker submitted multiple such transactions per block. Over 14 consecutive blocks (6191896 to 6191909, ~175 seconds), normal transaction throughput dropped from ~92 transactions per block to as few as 3. Legitimate Fomo3D key purchases could not fit in the gas-stuffed blocks, and the timer expired with the attacker as the last buyer.

INVALID’s role: The INVALID opcode was the core weapon. Its all-gas-consumption property made each attack transaction maximally expensive in terms of block gas usage, requiring fewer transactions to fill a block compared to alternatives (like infinite loops, which might be interrupted by gas estimation). The attacker paid ~3M), achieving a ~30,000% ROI.

Impact: ~$3M stolen. Established assert(false) / INVALID as the canonical block-stuffing primitive. Demonstrated that all-gas-consumption is a weaponizable property of the EVM.

References:


Exploit 2: RAI Liquidation Engine — Returndata Bombing Causes assert()-Like Gas Destruction (2023, Disclosed)

Root cause: A malicious SAFE savior contract could exploit the returndata copying mechanism to cause out-of-gas conditions in RAI’s liquidation engine, functionally equivalent to hitting INVALID — all forwarded gas is destroyed, and liquidations become impossible.

Details: RAI’s liquidation system allows “savior” contracts to intervene and save positions from liquidation. When a SAFE is being liquidated, the system calls saveSAFE() on the registered savior contract. The vulnerability exploited the interaction between the 63/64 gas rule and memory expansion costs:

  1. The savior contract receives 63/64 of the liquidation transaction’s gas.
  2. A malicious savior deliberately reverts with a massive return data payload (hundreds of kilobytes).
  3. The catch clause in the liquidation engine attempts to copy this returndata into memory via Solidity’s implicit returndatacopy.
  4. The memory expansion cost for the massive returndata exceeds the remaining 1/64 gas reserve.
  5. The liquidation function runs out of gas and reverts entirely.

The end result is identical to hitting INVALID: all gas is consumed, the transaction reverts, and the liquidation cannot proceed. The malicious savior effectively makes its SAFE unliquidatable, creating protocol insolvency risk. While the mechanism is not literally the 0xFE opcode, the security impact — all-gas destruction preventing critical protocol operations — is the same pattern.

INVALID’s relevance: This exploit demonstrates the broader category of “all-gas-consumption” attacks that INVALID epitomizes. The gas destruction pattern — whether via 0xFE, out-of-gas from memory expansion, or other mechanisms — is the root threat. Pre-0.8.0 contracts that use assert() in liquidation or settlement paths are vulnerable to the exact same denial-of-service: trigger the assert failure to burn all gas and prevent the operation from completing.

Impact: Critical vulnerability in RAI’s liquidation engine. Could prevent liquidations, leading to protocol insolvency. Remained unpatched as of January 2026.

References:


Exploit 3: Pre-0.8.0 Assert Failures in Production DeFi — Recurring Gas Drain Incidents (2019-2021)

Root cause: Major DeFi protocols deployed with Solidity < 0.8.0 used assert() for conditions that could be triggered by external state changes, causing all-gas destruction for users and bots interacting with the contracts during edge conditions.

Details: Multiple DeFi protocols experienced incidents where assert failures in production destroyed user gas:

  • Compound V2 liquidation edge cases. Early versions of Compound’s cToken contracts contained assert() checks in internal accounting functions. During rapid price movements, certain liquidation sequences could trigger these assertions, causing liquidation bots to lose their entire gas allocation on failed transactions. The bots would retry with higher gas limits, amplifying the gas cost. Compound later upgraded to use require() for conditions that external state could trigger.

  • Uniswap V2 invariant checks. The Uniswap V2 Pair contract uses assertions to verify the constant product formula (x * y >= k). While designed to catch accounting bugs, these assertions can theoretically fail during flash loan callbacks if the borrower does not return sufficient funds. Pre-0.8.0, this would burn all gas rather than cleanly reverting. The Uniswap V2 contracts compiled with Solidity 0.6.6 remain deployed and actively used.

  • Aave V1/V2 assert patterns. Early Aave contracts used assert for internal invariants that could be invalidated by oracle price manipulation or flash loan interactions. A failed assert in a lending pool operation destroyed all gas for the transaction sender, making the failure more expensive than necessary.

INVALID’s role: In all these cases, the INVALID opcode was the mechanism converting an internal consistency check failure into maximum gas destruction. The same conditions in post-0.8.0 contracts would produce a clean REVERT with a Panic code, refunding unused gas.

Impact: Cumulative gas losses across liquidation bots and users in the hundreds of ETH range over 2019-2021. Motivated the Solidity team’s decision to replace INVALID with REVERT + Panic codes in 0.8.0.

References:


Attack Scenarios

Scenario A: Block Stuffing via assert(false) Gas Burn

contract BlockStuffer {
    ITargetGame immutable game;
 
    constructor(ITargetGame _game) { game = _game; }
 
    function attack() external {
        // Check if we are the current leader in the game
        (, address leader, uint256 timeLeft) = game.getCurrentRoundInfo();
 
        if (leader == address(this) && timeLeft < 120) {
            // We're winning and the timer is almost up.
            // Burn ALL remaining gas to fill the block, preventing
            // other players from buying keys and resetting the timer.
            // Pre-0.8.0: assert(false) compiles to INVALID (0xFE),
            // which consumes all remaining gas -- maximally efficient
            // block stuffing.
            assert(false);
        }
    }
 
    // Attacker submits multiple attack() calls per block with
    // gas_limit = 30_000_000 and high gas price.
    // Each assert(false) burns 30M gas, filling the block.
    // Over 14+ blocks, legitimate transactions are excluded.
}

Scenario B: Gas Griefing via Forced assert() Failure in External Contract

// Pre-0.8.0 contract with assert-based invariant
contract LegacyVault {
    mapping(address => uint256) public balances;
    uint256 public totalDeposits;
 
    function deposit() external payable {
        balances[msg.sender] += msg.value;
        totalDeposits += msg.value;
    }
 
    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "insufficient");
        balances[msg.sender] -= amount;
        totalDeposits -= amount;
 
        // VULNERABLE: If totalDeposits underflows due to a bug
        // elsewhere, this assert burns ALL gas. In pre-0.8.0,
        // this is INVALID (0xFE), not REVERT.
        assert(address(this).balance >= totalDeposits);
 
        (bool ok,) = msg.sender.call{value: amount}("");
        require(ok, "transfer failed");
    }
 
    // If another function has a bug that desynchronizes
    // totalDeposits from the actual balance, EVERY subsequent
    // withdraw() call burns all gas via INVALID.
    // Users cannot withdraw their funds without losing their
    // entire gas allocation on each attempt.
}

Scenario C: Cascading Gas Destruction in Multi-Hop Call Chain

contract DEXAggregator {
    function executeSwap(
        address[] calldata path,
        uint256 amountIn
    ) external {
        uint256 currentAmount = amountIn;
 
        for (uint256 i = 0; i < path.length - 1; i++) {
            // Forward all remaining gas to each swap.
            // If ANY pool in the path hits INVALID (e.g., a pre-0.8.0
            // pool with an assert failure), 63/64 of remaining gas
            // is destroyed at that hop.
            (bool success, bytes memory result) = path[i].call(
                abi.encodeWithSelector(
                    IPool.swap.selector,
                    currentAmount
                )
            );
 
            // Even if we catch the failure, the gas is already gone.
            // The aggregator has only 1/64 reserve left -- likely
            // insufficient to continue the loop or revert cleanly.
            if (!success) revert("swap failed");
            currentAmount = abi.decode(result, (uint256));
        }
    }
 
    // Attack: Include a pre-0.8.0 pool in the swap path that has a
    // triggerable assert() failure. When the aggregator routes through
    // it, the INVALID opcode destroys 63/64 of all remaining gas.
    // The user pays for the full gas limit but gets nothing.
}

Scenario D: Undefined Opcode in Hand-Written Assembly

contract UnsafeAssembly {
    function dispatch(uint8 action, bytes calldata data) external {
        assembly {
            switch action
            case 0 { /* ... action 0 logic ... */ }
            case 1 { /* ... action 1 logic ... */ }
            case 2 { /* ... action 2 logic ... */ }
            // No default case! If action >= 3, execution falls through
            // to whatever bytes follow in the bytecode.
            // If those bytes happen to be undefined opcodes (0xB0, 0xC0, etc.)
            // or the compiler's trailing 0xFE padding, ALL gas is consumed.
        }
 
        // A user calling dispatch(255, "") loses their entire gas allocation
        // because execution hits an undefined opcode or the trailing INVALID guard.
    }
}

Mitigations

ThreatMitigationImplementation
T1: Gas griefing via INVALIDLimit gas forwarded to untrusted subcallsUse addr.call{gas: fixedLimit}(...) instead of forwarding all gas. Cap subcall gas to the minimum needed for the operation.
T1: Block stuffingProtocol-level mitigation only; cannot be prevented at contract levelEIP-1559 makes block stuffing more expensive (base fee increases with full blocks). Time-based games should use block numbers, not wall-clock timers.
T2: assert() vs require() confusionUse require() for all input validation; reserve assert() for true invariantsCompile with Solidity >= 0.8.0 where assert uses REVERT + Panic codes. Audit pre-0.8.0 contracts for assert misuse.
T2: Legacy pre-0.8.0 contractsUpgrade to Solidity >= 0.8.0 or wrap legacy calls with gas limitsIf upgrade is not possible, add gas-limited wrapper contracts that cap forwarded gas when calling legacy code.
T3: Undefined opcodes in bytecodeUse compiler-generated code; avoid hand-written assembly with undefined opcodesDeploy only compiler-generated bytecode. If using assembly, include explicit revert(0, 0) default cases in switch statements.
T4: Compiler-generated INVALID as guardNo mitigation needed; this is a safety featureUnderstand that hitting compiler-generated INVALID means a bug exists. Investigate the root cause rather than suppressing the symptom.
T5: Cascading gas destruction in call chainsLimit gas per subcall; use try/catch with sufficient reserved gastry target.func{gas: 100000}() { ... } catch { ... } ensures the catch block has enough gas to handle the failure.
General: Gas accounting in multi-contract protocolsBudget gas explicitly for each external callCalculate worst-case gas for each subcall and set explicit gas limits rather than forwarding gasleft().

Compiler/EIP-Based Protections

  • Solidity >= 0.8.0 (Panic Codes): Replaced INVALID with REVERT + Panic(uint256) for assert failures, division by zero, overflow/underflow, and array out-of-bounds. This is the single most impactful mitigation — gas is preserved on failure, error data is returned, and try/catch works meaningfully.
  • EIP-141 (Designated INVALID): Formally reserves 0xFE as the designated invalid instruction, guaranteeing it will never be assigned non-halting behavior. Ensures assert(false) always aborts execution.
  • EIP-3541 (0xEF Reservation, London 2021): Prevents deployment of new contracts starting with 0xEF, reserving the prefix for EOF. Reduces the risk of accidentally deploying code with undefined opcodes in the leading position.
  • EIP-3670 (EOF Code Validation): Part of the EOF initiative. Validates that deployed bytecode contains only defined opcodes, rejecting contracts with undefined opcodes at deploy time. Eliminates the forward-compatibility hazard of undefined opcodes for EOF-formatted contracts.
  • EIP-150 (63/64 Gas Rule, Tangerine Whistle 2016): Limits gas forwarded to subcalls to 63/64, ensuring the caller retains a 1/64 reserve. This prevents a single INVALID in a subcall from destroying 100% of the transaction’s gas, though 63/64 destruction is still severe.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighMediumFomo3D block stuffing ($3M, 2018)
T2Smart ContractHighHighRecurring gas drain in pre-0.8.0 DeFi protocols; SWC-110, OWASP SCWE-067
T3Smart ContractMediumLowNo major exploit; theoretical forward-compatibility risk mitigated by EOF
T4Smart ContractLowLowProtective measure; not a vulnerability
T5Smart ContractMediumMediumRAI liquidation engine gas destruction (2023); aggregator gas griefing
P1ProtocolMediumLowNo consensus bug from undefined opcodes to date; EOF initiative addresses this
P2ProtocolLowLowEIP-4337 gas accounting edge cases with INVALID

OpcodeRelationship
REVERT (0xFD)The safe counterpart to INVALID. REVERT halts execution, rolls back state changes, and refunds unused gas. Post-0.8.0, Solidity uses REVERT with Panic codes for all conditions that previously used INVALID. The gas behavior difference is the critical distinction: REVERT preserves gas, INVALID destroys it.
STOP (0x00)Normal execution halt. STOP ends execution successfully, persists state changes, and refunds unused gas. STOP is the “happy path” terminator; INVALID is the “catastrophic failure” terminator.
RETURN (0xF3)Normal execution halt with return data. Like STOP, RETURN ends execution successfully and refunds unused gas. RETURN can pass data back to the caller; INVALID cannot return any data.