Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0xFD |
| Mnemonic | REVERT |
| Gas | 0 (static) + memory expansion cost for the return data region |
| Stack Input | offset (byte offset in memory), length (byte length of return data) |
| Stack Output | (none — execution halts) |
| Behavior | Halts execution of the current call context, reverts all state changes (storage writes, balance transfers, log entries) made within that context, copies length bytes from memory starting at offset into the return data buffer, and refunds all remaining gas to the caller. Introduced in EIP-140 (Byzantium, October 2017). Unlike INVALID (0xFE), which consumes all gas, REVERT provides a gas-efficient failure path. Unlike STOP (0x00) or RETURN (0xF3), REVERT signals failure and rolls back state. |
Threat Surface
REVERT is the EVM’s primary mechanism for controlled failure. It underpins Solidity’s require(), revert(), custom errors, and assert() (post-0.8.0). Because REVERT both returns data and refunds gas, it creates a unique threat surface that spans information leakage, denial of service, and gas economics manipulation.
The threat surface centers on five properties:
-
REVERT refunds remaining gas, creating asymmetric economic incentives. When a sub-call reverts, the caller gets back all gas not consumed by the callee. This is the correct behavior for honest failures, but it means a malicious callee can revert cheaply while forcing the caller to spend gas on the call setup, memory expansion, and return data copying. An attacker who can force a contract to repeatedly call a reverting target wastes the victim’s gas without the attacker paying a proportional cost. This is the foundation of gas griefing attacks.
-
Revert reason strings leak information. Solidity’s
require(condition, "reason")andrevert("reason")encode a human-readable string into the return data usingError(string)ABI encoding. Custom errors (revert InsufficientBalance(required, available)) encode structured data. Both are visible to anyone inspecting the transaction — on-chain via RETURNDATACOPY, off-chain viaeth_callor trace APIs. Contracts that include sensitive state information (balances, internal addresses, configuration values) in revert reasons expose that data to attackers who can probe the contract with crafted inputs and read the revert data. -
REVERT only rolls back the current call context. When contract A calls contract B and B reverts, only B’s state changes are undone. A’s state changes before and after the call persist. This is well-understood by experienced developers, but it creates subtle bugs when contracts assume a reverted sub-call has no side effects. Specifically, if A modifies its own state, calls B (which reverts), and then continues execution, A’s state may be inconsistent with what B would have produced. The
try/catchpattern in Solidity makes this explicit, but developers frequently mishandle the catch path. -
Return data from REVERT can be weaponized. The revert data buffer has no protocol-enforced size limit — a reverting contract can return megabytes of data. The caller pays for memory expansion to store this data (quadratic cost above 724 bytes), even if it never reads the data. This “returndata bombing” attack forces the caller to consume gas proportional to the revert data size, potentially causing an out-of-gas revert in the caller. Solidity’s
try/catchand high-level calls automatically copy all return data into memory, making them vulnerable by default. -
Custom errors vs. string reasons have different ABI encoding.
Error(string)uses selector0x08c379a0while custom errors use their own 4-byte selectors. Contracts that parse revert data (e.g., wrapping errors, retry logic, or cross-contract error propagation) must handle both formats. Malformed revert data — data that doesn’t match either encoding — can causeabi.decodeto revert, creating a secondary failure path. A malicious contract can craft revert data that passes initial checks but causes decoding failures in the caller.
Smart Contract Threats
T1: Revert Reason Information Leakage (Medium)
Revert reason strings and custom errors are part of the transaction’s return data, visible to any party that executes or simulates the transaction. Contracts that embed sensitive state in revert messages create an oracle that attackers can query for free via eth_call (no gas cost, no on-chain footprint):
-
Balance and threshold exposure. A pattern like
require(balance >= amount, string.concat("Insufficient: have ", toString(balance)))reveals the exact balance to any caller who triggers the revert. Attackers use this to probe token balances, collateral ratios, or liquidity pool reserves without relying on public getter functions that might have access controls. -
Internal address leakage. Revert messages that include contract addresses (e.g.,
revert("Unauthorized: expected ", expectedAdmin)) reveal internal configuration that may not be exposed through any public interface. -
State-dependent error paths as timing oracles. Different revert reasons for different failure conditions let attackers enumerate internal state. If
withdraw()reverts with “Insufficient balance” vs. “Withdrawal locked” vs. “Cooldown active,” an attacker can determine the contract’s exact state by testing each condition. -
Custom error structured data. Custom errors like
error InsufficientLiquidity(uint256 available, uint256 required)are more gas-efficient than strings but expose structured data that is trivially ABI-decoded. The data is more machine-readable than string reasons, making automated probing easier.
Why it matters: Information leakage via revert reasons enables targeted attacks. An attacker who knows exact balances, thresholds, or lock states can craft transactions that exploit the contract at precisely the right moment.
T2: Gas Griefing via Returndata Bombing (High)
A malicious contract can revert with an arbitrarily large return data payload, forcing the caller to pay for memory expansion. EVM memory costs grow quadratically: the first 724 bytes are roughly linear (3 gas per word), but beyond that, the cost includes a quadratic term (memory_size_word² / 512). A revert returning 1 MB of data costs the caller approximately 3.2 billion gas in memory expansion alone — far more than any block gas limit:
-
try/catch vulnerability. Solidity’s
try/catchcopies all return data into memory before the catch block executes. A malicious external contract that reverts with a large payload causes the caller to OOG (out-of-gas) before reaching the catch handler. The caller’s transaction reverts entirely, defeating the purpose of the try/catch. -
Low-level call vulnerability. Even
(bool success, bytes memory data) = target.call(...)copies the full return data intodata. The memory expansion cost is incurred during the copy, not during the external call itself, so the caller pays regardless of whether it inspectsdata. -
Liquidation griefing. DeFi protocols that use try/catch to handle failures during liquidation (e.g., calling a “savior” contract or unwinding collateral) are vulnerable. A malicious position holder deploys a contract that reverts with massive data when liquidated, rendering the position unliquidatable and threatening protocol solvency. This was demonstrated in the RAI protocol vulnerability (2023).
-
Cross-contract DoS. Any protocol that makes external calls to untrusted contracts (governance execution, callback hooks, oracle queries with fallbacks) can be griefed by returndata bombing if it doesn’t cap the return data size.
Why it matters: Returndata bombing turns REVERT into an offensive gas weapon. The attacker pays almost nothing (the revert itself is cheap), while the caller pays quadratic memory costs that can exceed the block gas limit.
T3: Denial of Service via Intentional Revert (High)
A contract that makes external calls as part of its critical path can be permanently blocked if one of those calls always reverts. This is the most common REVERT-related vulnerability class:
-
Push-over-pull payment DoS. A contract that iterates over recipients and sends ETH (e.g., refunding auction bidders, distributing rewards) will revert the entire transaction if any single recipient’s fallback reverts. An attacker deploys a contract with
receive() external payable { revert(); }, becomes a recipient, and permanently blocks all future payments. -
Auction/bidding griefing. In an auction contract where the previous highest bidder must be refunded before accepting a new bid, a malicious bidder deploys a reverting contract as the bidder address. All subsequent bids fail because the refund to the malicious bidder reverts, and the malicious bidder wins the auction at a below-market price.
-
Governance execution blocking. Governance proposals that execute a sequence of actions (e.g., Compound’s GovernorBravo
execute()) will revert entirely if any single action in the sequence reverts. An attacker who can make one target action revert (e.g., by front-running to change state) can block the entire proposal. -
Callback-based protocol DoS. Protocols that call user-supplied callback contracts (flash loans, hooks, saviours) are vulnerable if the callback reverts. The protocol’s critical function (liquidation, settlement, epoch advancement) is blocked until the reverting callback is removed.
Why it matters: Intentional revert DoS is cheap (a reverting fallback is ~1 byte of deployed code), persistent (the reverting contract remains deployed), and can block contracts holding millions in value.
T4: Revert Data Manipulation and Spoofing (Medium)
Contracts that parse or forward revert data from sub-calls can be manipulated by crafting specific revert payloads:
-
Error selector spoofing. A malicious contract can revert with data that mimics a known error selector (e.g.,
0x08c379a0forError(string)) but contains malformed ABI encoding. Callers thatabi.decodethe revert data will themselves revert on the malformed input, creating a secondary DoS vector. -
Error wrapping confusion. Contracts that wrap revert data from sub-calls (e.g., ERC-7751
WrappedError) must handle the case where the inner revert data is adversarial. If the wrapping logic ABI-encodes the raw revert bytes without length validation, the wrapped error can exceed gas limits or contain data that confuses upstream error parsers. -
try/catch selector-based routing. Solidity’s
try/catchcan catch specific error types:catch Error(string memory reason)catchesError(string)reverts,catch (bytes memory data)catches everything else. A malicious contract that reverts with anError(string)selector but invalid string encoding causes the catch block to revert during ABI decoding, bypassing both catch handlers. -
Cross-chain revert data relay. Bridge contracts that relay revert data from one chain to another must sanitize the data. A malicious revert on the source chain could contain data that exploits parsing logic on the destination chain.
Why it matters: Revert data is untrusted input from external contracts. Any parsing, forwarding, or decoding of revert data without validation creates attack surface.
T5: REVERT in Constructor Prevents Deployment — Factory DoS (Medium)
When a constructor executes REVERT, the contract deployment fails and no code is stored at the target address. For factory contracts that deploy child contracts, a constructor revert means the factory’s CREATE or CREATE2 call returns address(0):
-
Factory griefing via CREATE2 address squatting. If a factory uses
CREATE2with a user-controlled salt, an attacker can predict the deployment address and front-run the legitimate deployer with a deployment that reverts. The address is now “used” for that salt (the nonce is consumed), and the legitimate deployer’s transaction either fails or gets a different address than expected, depending on the factory’s implementation. -
Conditional constructor revert. A malicious implementation contract can include constructor logic that reverts under specific conditions (e.g., based on block number, caller address, or constructor arguments). The factory succeeds in testing but fails in production, creating intermittent deployment failures that are hard to diagnose.
-
Gas-limited constructor revert. If the factory forwards limited gas to the constructor (e.g., via
CREATEwith a bounded gas budget), a legitimate constructor that needs more gas will revert, and the factory may not distinguish between “constructor ran out of gas” and “constructor intentionally reverted.” Both returnaddress(0). -
Proxy initialization revert. Proxy factories that deploy a minimal proxy and then call
initialize()in the same transaction risk partial failure: the proxy deploys successfully (CREATE doesn’t revert), but the initialization reverts. The proxy exists but is uninitialized, and if the initialization function lacks re-initialization guards, an attacker can call it and take ownership.
Why it matters: Factory-deployed contracts are the backbone of DeFi (Uniswap pairs, Aave markets, Compound cTokens). A factory that can be griefed into failing deployments disrupts the entire protocol.
Protocol-Level Threats
P1: REVERT Gas Refund vs. INVALID Gas Consumption (Low)
REVERT (0xFD) refunds all remaining gas to the caller, while INVALID (0xFE) consumes all gas. This distinction creates a protocol-level asymmetry:
-
Compiler behavior. Solidity >= 0.8.0 compiles
assert()to REVERT (withPanic(uint256)error code) instead of INVALID. This means failed assertions no longer burn all gas, changing the economic impact of assertion failures from total gas loss to minimal gas loss. Contracts compiled with older Solidity versions still use INVALID forassert(). -
Client resource consumption. From a node operator perspective, REVERT is cheaper to process than a successful transaction because reverted state changes don’t need to be committed. However, the node still executes all opcodes up to the REVERT point and must process the return data buffer.
-
Gas estimation interaction.
eth_estimateGasmust execute the transaction to determine gas usage. If a transaction reverts, some clients return the revert data in the error response, exposing revert reasons to off-chain callers without any on-chain cost. This amplifies the information leakage risk from T1.
P2: REVERT and Transaction Inclusion Incentives (Low)
Reverted transactions are still included in blocks and still pay the base fee (post-EIP-1559). The validator earns the priority fee regardless of whether the transaction succeeds or reverts:
-
Reverted transactions consume block space. A reverted transaction takes gas from the block gas limit. Validators have no economic incentive to exclude reverted transactions (they collect fees either way), and may even prefer them if the priority fee is high.
-
Spam via cheap reverts. An attacker can submit transactions that revert early (consuming minimal gas but still occupying a transaction slot and paying base fee). At low gas prices, this is a cheap way to inflate mempool size and slow block propagation, though the base fee mechanism limits the economic feasibility.
P3: REVERT Semantics Across EVM Chains (Medium)
REVERT behavior is consistent across all EVM-compatible chains (it’s part of the core EVM spec since Byzantium), but the surrounding infrastructure differs:
-
Revert reason availability. Some L2s and sidechains don’t expose revert reasons in
eth_callresponses or transaction receipts by default. Contracts that rely on parsing revert reasons for cross-contract error handling may silently fail on chains that strip revert data. -
Gas pricing differences. Memory expansion costs (and therefore the cost of large revert data) may differ on L2s with modified gas schedules. A returndata bombing attack that is block-gas-limited on L1 might be feasible within L2 gas limits if the L2 has cheaper memory or higher block gas limits.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
REVERT with empty data (length = 0) | Execution halts, state reverts, return data buffer is empty | Callers checking revert data length get 0. Solidity catch (bytes memory data) receives empty bytes. Cannot distinguish between “intentional empty revert” and “unknown failure” without the success flag. |
| REVERT with very large data | Memory expands to accommodate offset + length bytes; caller pays quadratic memory expansion cost to copy the data | Returndata bombing: a malicious callee forces the caller to OOG during return data copying. try/catch doesn’t help because memory expansion happens before the catch block executes. |
| REVERT in constructor | Deployment fails; no code is stored; CREATE/CREATE2 returns address(0) | Factory contracts must check the return address. Partial initialization (proxy deployed but initialize() reverts) leaves uninitialized proxies that can be hijacked. |
REVERT in DELEGATECALL | State changes in the caller’s storage context are reverted (since delegatecall executes in the caller’s context); the caller’s execution continues if using low-level call | Confusing mental model: the “current call context” for REVERT is the delegatecall frame, but state changes being reverted are in the delegating contract’s storage. The delegating contract sees success = false and must handle the partial state. |
| Nested REVERT (sub-call reverts, caller reverts) | Each REVERT only unwinds its own call frame. If caller also reverts, the caller’s state changes are also rolled back. Gas refunded from inner revert is consumed or refunded by outer frame. | Deep call stacks with multiple revert levels make gas accounting complex. A revert at depth N refunds gas to depth N-1, which may then also revert and refund to N-2. Total gas consumption depends on where in the stack the reverts happen. |
| REVERT at the top-level transaction | Entire transaction state is reverted; gas up to the REVERT point is consumed; remaining gas is refunded to the sender | The transaction is still included in the block and still pays the base fee + consumed gas. The sender loses gas spent before the revert but recovers unspent gas. |
REVERT with offset + length causing memory expansion but reading uninitialized memory | Memory beyond current size is zero-initialized; the revert data contains zeros for the uninitialized region | No direct security issue, but contracts parsing revert data may misinterpret zero-padded data as valid ABI-encoded content. |
| REVERT after SSTORE | All storage writes in the current call context are reverted; gas refunds for clearing storage slots (EIP-3529) do not apply to reverted writes | No gas refund is given for SSTORE operations that are undone by REVERT. The gas cost of the SSTORE is already consumed. |
Solidity require() vs revert vs assert() | All compile to REVERT (>= 0.8.0). require uses Error(string), assert uses Panic(uint256), custom revert uses the custom error selector. | Different error selectors for the same opcode. Contracts parsing revert data must handle all three formats plus raw revert (no selector). |
Real-World Exploits
Exploit 1: Akutars NFT — $34M ETH Locked via Revert-Based DoS (April 2022)
Root cause: The Akutars NFT auction contract used a push-based refund pattern where processRefunds() iterated over bidders and sent ETH refunds. A single reverting recipient could block all refunds. Combined with a separate logic bug in claimProjectFunds(), the contract permanently locked 11,539.5 ETH.
Details: The processRefunds() function used call to send ETH to each bidder in sequence. If any bidder was a contract whose receive() function reverted (or consumed all forwarded gas), the entire processRefunds() transaction would revert, blocking refunds for all subsequent bidders. A security researcher (“USER221”) demonstrated the DoS vulnerability by deploying a reverting contract as a bidder, proving the auction could be griefed.
The more devastating bug was in claimProjectFunds(), which required refundProgress >= totalBids. Because refundProgress tracked unique users (3,669) while totalBids tracked total bids (5,495, since users could bid multiple times), this condition could never be satisfied. The project team could never withdraw funds.
REVERT’s role: The DoS attack was a textbook REVERT griefing: a malicious contract’s receive() function executed REVERT, which propagated up through the call and caused the parent processRefunds() to revert. The attacker paid almost nothing (REVERT refunds gas), while the contract was permanently blocked.
Impact: 11,539.5 ETH (~$34M) locked in the contract. The security researcher eventually cooperated to resolve the situation, but the vulnerability was fully exploitable by a malicious actor.
References:
- Halborn: Akutars NFT Incident Explained
- Beosin: $34M Locked Due to Contract Vulnerabilities in Akutar
Exploit 2: King of the Ether Throne — Revert-Based Auction Griefing (February 2016)
Root cause: The King of the Ether Throne (KotET) contract sent ETH refunds to the previous “king” using send() (which forwards only 2300 gas) during the throne-claiming process. If the previous king was a contract that reverted on receive, no new king could be crowned.
Details: The KotET game allowed players to claim the “throne” by paying more than the current king. The contract attempted to refund the previous king in the same transaction. Two failure modes existed:
-
Silent send failure. The original contract used
address.send()without checking the return value. When sending to Ethereum Mist wallet contracts (which needed more than 2300 gas to process a receive), the send silently failed and the refund was lost. -
Explicit revert griefing. An attacker could deploy a contract with
receive() external payable { revert(); }and claim the throne. Any subsequent attempt to claim the throne would fail because the refund to the previous king’s contract always reverted. Withrequire(success)on the send, the entire transaction reverts. Without it, the previous king loses their refund.
REVERT’s role: The griefing variant specifically uses REVERT in the malicious contract’s fallback to block the refund path. Before EIP-140 (Byzantium, 2017), contracts used INVALID for this purpose (burning all gas). Post-Byzantium, REVERT makes the griefing cheaper because the attacker’s fallback refunds gas.
Impact: ETH refunds were lost for multiple players. The incident became one of the canonical examples of the “push vs. pull” payment pattern and is referenced in virtually every smart contract security course.
References:
Exploit 3: RAI Protocol — Returndata Bombing Makes Positions Unliquidatable (September 2023)
Root cause: RAI (Reflexer Finance) protocol’s LiquidationEngine used a try/catch pattern when calling user-configured “savior” contracts during liquidation. A malicious savior could revert with megabytes of data, causing the try/catch to OOG during return data copying, making the position permanently unliquidatable.
Details: RAI’s Safe Saviours feature allowed users to configure a contract that would be called before their collateralized debt position was liquidated, giving the savior contract a chance to add collateral or repay debt. The LiquidationEngine called the savior using Solidity’s try/catch to gracefully handle savior failures.
The vulnerability: Solidity’s try/catch copies all return data from the external call into memory before executing the catch block. A malicious savior that reverted with a very large payload (e.g., revert(0, 10000000) in assembly returning ~10 MB of zeros) forced the LiquidationEngine to allocate megabytes of memory. The quadratic memory expansion cost consumed all remaining gas before the catch block could execute, causing the entire liquidateSAFE() call to revert.
The attacker could create a collateralized position, set the malicious savior, and the position would become permanently unliquidatable. With enough unliquidatable bad debt, the protocol could become insolvent.
REVERT’s role: REVERT was the weapon — the malicious savior used REVERT with a massive length parameter to produce gigantic return data. The EVM faithfully copied this data into the caller’s memory space, incurring quadratic gas costs that exceeded the block gas limit. The try/catch, designed to protect against savior failures, was defeated by the return data size.
Impact: Critical vulnerability in RAI protocol. No funds were stolen (discovered via bug bounty), but the vulnerability could have caused protocol insolvency through unliquidatable positions. As of early 2026, the fix required using assembly-level calls that explicitly limit return data size.
References:
- Trust Security: Returndata Bombing RAI’s Liquidation Engine
- Kadenzipfel: Unbounded Return Data Vulnerability
Attack Scenarios
Scenario A: Push-Payment DoS via Reverting Recipient
contract VulnerableAuction {
address public highestBidder;
uint256 public highestBid;
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
address previousBidder = highestBidder;
uint256 previousBid = highestBid;
highestBidder = msg.sender;
highestBid = msg.value;
// VULNERABLE: if previousBidder is a contract that reverts,
// no one can ever place a new bid
(bool success,) = previousBidder.call{value: previousBid}("");
require(success, "Refund failed");
}
}
contract AuctionGriefing {
function attack(VulnerableAuction auction) external payable {
auction.bid{value: msg.value}();
}
// All incoming ETH is rejected -- permanently blocks the auction
receive() external payable {
revert();
}
}Scenario B: Returndata Bombing via try/catch
contract VulnerableLiquidator {
function liquidate(address position, address savior) external {
// VULNERABLE: try/catch copies all return data into memory.
// A malicious savior can revert with megabytes of data,
// causing OOG before the catch block executes.
try ISavior(savior).save(position) returns (bool saved) {
if (saved) return;
} catch {
// Never reached if returndata is too large
}
_forceLiquidate(position);
}
function _forceLiquidate(address position) internal { /* ... */ }
}
contract MaliciousSavior {
// Reverts with ~1 MB of data, forcing quadratic memory expansion
// in the caller's try/catch return data copy
fallback() external {
assembly {
revert(0, 1000000)
}
}
}Scenario C: Information Leakage via Revert Probing
contract VulnerableVault {
mapping(address => uint256) private balances;
mapping(address => uint256) private lockUntil;
function withdraw(uint256 amount) external {
// VULNERABLE: revert reasons expose exact internal state
require(
block.timestamp >= lockUntil[msg.sender],
string.concat("Locked until: ", Strings.toString(lockUntil[msg.sender]))
);
require(
balances[msg.sender] >= amount,
string.concat("Balance: ", Strings.toString(balances[msg.sender]))
);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
// Attack: Attacker calls withdraw() via eth_call (free, off-chain)
// with various amounts to extract exact balance and lock time
// from the revert reason strings. No on-chain transaction needed.Scenario D: Revert Data Decoding DoS
contract VulnerableErrorHandler {
function safeCall(address target, bytes calldata data) external returns (bytes memory) {
(bool success, bytes memory result) = target.call(data);
if (!success) {
// VULNERABLE: abi.decode on attacker-controlled revert data.
// If the data has Error(string) selector but malformed encoding,
// abi.decode reverts, and the catch-all error path is never reached.
if (result.length >= 4) {
bytes4 selector = bytes4(result);
if (selector == 0x08c379a0) {
string memory reason = abi.decode(
_slice(result, 4, result.length - 4),
(string)
);
revert(reason);
}
}
revert("Unknown error");
}
return result;
}
function _slice(bytes memory data, uint256 start, uint256 length)
internal pure returns (bytes memory)
{
bytes memory result = new bytes(length);
for (uint256 i = 0; i < length; i++) {
result[i] = data[start + i];
}
return result;
}
}
// A malicious target reverts with selector 0x08c379a0 followed by
// invalid ABI encoding (e.g., string length pointing past data end).
// abi.decode reverts, and safeCall's "Unknown error" fallback is never hit.Scenario E: REVERT in Constructor — Factory Deployment Griefing
contract VulnerableFactory {
function deploy(bytes32 salt, bytes calldata initCode) external returns (address) {
address child;
assembly {
child := create2(0, add(initCode.offset, 0x20), initCode.length, salt)
}
// VULNERABLE: if the constructor reverts, child == address(0).
// An attacker can front-run with the same salt to "burn" it.
require(child != address(0), "Deployment failed");
return child;
}
}
// Attack: attacker monitors the mempool for deploy() calls.
// They front-run with the same salt but initCode that reverts in the constructor.
// The CREATE2 address is "used" (nonce consumed), and the legitimate
// deployer's transaction fails because the salt is taken.
// On some factory designs, the attacker can repeatedly grief new salts.Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Revert reason info leakage | Use generic error messages; avoid embedding state values in revert strings | Replace require(bal >= amt, concat("Balance: ", toString(bal))) with revert InsufficientBalance() (no parameters, or only non-sensitive ones) |
| T1: State probing via eth_call | Limit information in custom errors; treat revert data as public | Design custom errors that convey the error type but not exact internal values; e.g., error InsufficientBalance() instead of error InsufficientBalance(uint256 available, uint256 required) |
| T2: Returndata bombing | Cap return data size using assembly-level calls | assembly { success := call(gas(), target, 0, inPtr, inLen, 0, 0) returndatacopy(outPtr, 0, min(returndatasize(), MAX_RETURN_SIZE)) } — never copy unbounded return data |
| T2: try/catch OOG | Replace try/catch with low-level call + bounded returndatacopy | EigenLayer’s pattern: use assembly call, then copy at most 32 bytes of return data for success/failure detection |
| T3: Push-payment DoS | Pull-over-push pattern for ETH transfers | Store pending withdrawals in a mapping; let recipients call withdraw() themselves. Never iterate over recipients with ETH sends. |
| T3: Auction griefing | Separate refund from bid acceptance | Accept the new bid unconditionally, credit the previous bidder’s refund to a withdrawal balance, let them pull it later |
| T3: Governance execution DoS | Allow partial execution; don’t revert entire proposal on single action failure | Wrap each proposal action in a try/catch (with bounded return data); log failures but continue execution |
| T4: Revert data parsing | Validate revert data format before decoding; use try/catch around abi.decode | Check data length matches expected encoding before calling abi.decode; always have a fallback for malformed data |
| T5: Factory deployment DoS | Use deployer-specific salts; validate deployment success atomically | Include msg.sender in the CREATE2 salt to prevent front-running; deploy and initialize in the same transaction |
| T5: Uninitialized proxy | Initialize in the constructor or use _disableInitializers() | OpenZeppelin’s _disableInitializers() in the implementation constructor prevents post-deployment initialization hijacking |
| General: Gas-bounded external calls | Forward limited gas to untrusted callees | target.call{gas: MAX_CALLBACK_GAS}(data) — cap gas to prevent the callee from consuming all gas before reverting |
Compiler/EIP-Based Protections
- Solidity >= 0.8.0:
assert()compiles to REVERT withPanic(uint256)instead of INVALID, preserving remaining gas on assertion failures. This is a safety improvement for honest failures but does not affect the attack surface. - Solidity custom errors (>= 0.8.4): Custom errors are more gas-efficient than
Error(string)revert reasons (no dynamic string encoding). They also encourage structured, typed error data, which is harder to accidentally fill with sensitive information. - EIP-140 (Byzantium, 2017): Introduced REVERT. Before this, the only way to signal failure was INVALID (consuming all gas) or carefully crafted execution paths. REVERT made error handling practical but also made revert-based attacks gas-efficient.
- EIP-3529 (London, 2021): Reduced gas refund cap to 1/5 of transaction gas. While primarily targeting SSTORE refund abuse, it limits the total gas that can be refunded via reverted storage operations.
- EIP-7751 (proposed): Standardizes
WrappedErrorfor bubbling up revert reasons from sub-calls. Enables structured error propagation while allowing contracts to add context without exposing raw inner revert data.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Medium | High | Ongoing: revert reason probing is a standard reconnaissance technique |
| T2 | Smart Contract | High | Medium | RAI protocol returndata bombing (2023), documented in multiple audit reports |
| T3 | Smart Contract | High | High | Akutars NFT ($34M locked, 2022), King of the Ether (2016), OWASP Smart Contract Top 10 |
| T4 | Smart Contract | Medium | Low | Documented in Solidity compiler issues; exploitable in error-handling middleware |
| T5 | Smart Contract | Medium | Medium | CREATE2 front-running attacks on factory contracts; proxy initialization hijacking |
| P1 | Protocol | Low | N/A | REVERT vs. INVALID gas semantics (Solidity >= 0.8.0 compiler change) |
| P2 | Protocol | Low | Low | Reverted transaction spam is economically limited by EIP-1559 base fee |
| P3 | Protocol | Medium | Low | L2 revert reason stripping; cross-chain gas schedule differences |
Related Opcodes
| Opcode | Relationship |
|---|---|
| RETURN (0xF3) | The success counterpart to REVERT. Both halt execution and return data, but RETURN commits state changes while REVERT rolls them back. Both use the same memory region format (offset, length). |
| STOP (0x00) | Halts execution successfully with no return data. Like RETURN but with an empty return data buffer. STOP commits state changes; REVERT does not. |
| INVALID (0xFE) | The “hard failure” counterpart to REVERT. INVALID consumes all remaining gas and rolls back state, while REVERT refunds remaining gas. Pre-Solidity-0.8.0, assert() compiled to INVALID; now it compiles to REVERT with Panic(uint256). |
| CALL (0xF1) | Initiates a sub-call that may execute REVERT. The caller receives the revert data via RETURNDATASIZE/RETURNDATACOPY and sees success = 0 on the stack. CALL’s gas forwarding rules determine how much gas the callee has available to consume before reverting. |