DOS Attacks
How the attack works and how to prevent it
Denial of Service (DOS) Attacks in Smart Contracts: How They Work and How to Prevent Them
Denial of Service (DOS) vulnerabilities prevent users or administrators from completing an operation. In smart contracts, a blocked withdrawal or settlement path can leave funds inaccessible because deployed code cannot be restarted like a conventional server.
This guide explains external-call failures, unbounded loops, block stuffing, the Fomo3D and Akutars incidents, vulnerable Solidity patterns, and practical defenses.
What is a denial of service (DOS) attack in Web3?
A denial of service (DOS) attack exploits a condition that blocks one or more contract operations. The result may be failed withdrawals, frozen funds, or unavailable settlement and governance functions.
Unlike network-level DOS attacks that overwhelm servers with traffic, smart contract DOS usually exploits application logic. A single transaction can create a persistent failure when the contract has no recovery path.
The jammed door analogy
Consider a bank vault with an automated door that admits one customer at a time.
If one obstruction jams the door, the funds remain in the vault but customers cannot reach them.
A contract can fail in the same way: its balance remains visible on-chain, but a required operation can no longer complete.
Why DOS attacks matter
DOS vulnerabilities have caused both temporary disruption and permanent loss of access:
| Attack | Year | Impact |
|---|---|---|
| Parity Wallet Freeze | 2017 | $280M permanently frozen (513,774 ETH) |
| Akutars NFT | 2022 | $34M permanently locked (11,539 ETH) |
| Fomo3D Block Stuffing | 2018 | $3M stolen via block manipulation |
| GovernMental Ponzi | 2016 | 1,100 ETH locked (unbounded array) |
A smart contract cannot be restarted or patched unless its design includes an upgrade or recovery mechanism, so a permanent failure in a withdrawal path leaves the funds inaccessible.
The true cost: DOS vs. traditional hacks
A regular smart contract hack usually involves a bad guy draining your protocol's funds straight into their wallet. Classic theft.
DOS attacks are a completely different beast - they destroy access to funds. Think of it like dropping the keys to a treasure chest into the middle of the ocean. The money is still perfectly visible to the world on Etherscan, teasing everyone, but absolutely nobody can touch it. Take the infamous Parity Wallet bug for example: $280 million has been stuck frozen in time since 2017, and it will remain there, permanently untouched, forever.
The Smart Contract Hacking course includes lab exercises for identifying, reproducing, and fixing DOS vulnerabilities.
The Fomo3D block-stuffing attack
The 2018 Fomo3D incident showed how block-space competition could prevent time-sensitive transactions and determine a contract outcome.
What Fomo3D was
Launched in 2018, Fomo3D boldly branded itself as "a psychological social experiment in greed." And boy, did it deliver. The rules of the game were deceptively simple:
How the game worked:
-
Players bought "keys" with ETH, increasing the prize pot.
-
A countdown timer started ticking down from 24 hours.
-
Every time a player bought a new key, another 30 seconds were added to the clock.
-
When the timer finally hit zero, the very last person to buy a key walked away with the whole pot.
The design assumed someone would keep buying another key to become the final buyer.
By August 2018, everything was going according to plan. The grand prize pot had swelled to a mind-boggling 10,469 ETH, worth about $3 million at the time.
The attack: August 22, 2018
An anonymous attacker at address 0xa169... had a plan that nobody saw coming.
Step 1 - Buy the Last Key: The attacker purchased a key in block 6,191,896. The timer was counting down with very little time left.
Step 2 - Stuff the Blocks: Immediately after, the attacker began flooding the Ethereum mempool with transactions from a smart contract that used Solidity's assert() statement. When assert fails, it consumes all remaining gas via the 0xFE invalid opcode - unlike require() which refunds unused gas.
Each transaction was priced at 501 Gwei, far above normal prices, and each consumed roughly 8 million gas - nearly the entire block gas limit.
Step 3 - The Lockout: For 13 consecutive blocks (roughly 175 seconds), the attacker's transactions dominated every block. Block 6,191,906 contained only 3 transactions total. No one else could buy a Fomo3D key because there was zero room in any block.
Step 4 - Timer Expires: At block 6,191,909, the Fomo3D timer hit zero. The attacker was the last key buyer. The pot of 10,469 ETH was theirs.
The hacker's ROI: a 250x payday
The attacker spent an estimated 40-50 ETH (about $11,000 to $13,800) on gas and received 10,469 ETH ($3 million), roughly a 250x return at the values used in this example.
A Fomo3D developer stated that the strategy was "part of the game." Observers disagreed on whether the outcome was intended gameplay or an exploit of block-space mechanics.
Regardless of where you stand, the Fomo3D heist went down in history. It became the definitive, most-studied example of block-level DOS attacks and proved once and for all that Ethereum's block gas mechanism could absolutely be weaponized against time-sensitive contracts.
The flavors of destruction: types of DOS attacks
Just like ice cream, smart contract DOS attacks come in several distinct flavors - except these flavors exploit different architectural weaknesses and leave you permanently bankrupt.
Beyond these three primary types, block stuffing (filling entire blocks to prevent time-sensitive transactions), force-sending ETH via selfdestruct to break balance-dependent logic, and storage bloating (inflating arrays to make iteration impossible) are all active threat vectors used in real-world attacks.
How DOS attacks work, step by step
The most common vector is DOS with unexpected revert, and the whole attack fits in two transactions from the attacker.
The attack flow
| Step | What Happens | State |
|---|---|---|
| 1 | Vulnerable auction contract is deployed | Accepting bids normally |
| 2 | Attacker deploys a malicious contract | No receive() or fallback() function |
| 3 | Attacker bids through malicious contract | Becomes highest bidder |
| 4 | Legitimate user tries to outbid | Contract tries to refund attacker first |
| 5 | Refund to attacker's contract reverts | No fallback = ETH rejected |
| 6 | Entire bid transaction reverts | require(success) kills everything |
| 7 | All future bids fail the same way | Auction permanently frozen |
The attack phases
The attacker creates a contract with no receive() or fallback() function - or one that explicitly calls revert(). This contract physically cannot accept ETH.
The attacker calls the auction's bid() function through their malicious contract, becoming the highest bidder. The contract is now embedded in the auction's refund logic.
When anyone tries to outbid the attacker, the auction contract attempts to refund the previous highest bidder. But the refund always reverts because the attacker's contract rejects ETH.
Since the require(success) check fails, the entire transaction reverts. No new bids can ever succeed. The auction is permanently frozen - the attacker wins by making it impossible for anyone else to participate.
This principle - separating payment logic from core business logic via the Pull-over-Push pattern - prevents the most common DOS attacks.
{
"title": "🎬 Auction freeze: one bidder who refuses refunds locks everyone out",
"stage": { "width": 860, "height": 400 },
"nodes": [
{ "id": "honest", "label": "Honest Bidder", "role": "wants to bid 6 ETH", "emoji": "🧑", "x": 60, "y": 46, "color": "green" },
{ "id": "attacker", "label": "Attacker Contract", "role": "no receive() function", "emoji": "📜", "x": 630, "y": 46, "color": "red" },
{ "id": "auction", "label": "The Auction", "role": "refunds previous bid", "emoji": "🏛️", "x": 345, "y": 250, "color": "cyan" }
],
"links": [
{ "from": "honest", "to": "auction" },
{ "from": "auction", "to": "attacker" }
],
"nets": [
{ "id": "bid", "label": "Highest Bid" },
{ "id": "open", "label": "Can Others Bid?" }
],
"legend": [
{ "cls": "call", "label": "contract call" },
{ "cls": "token", "label": "ETH transfer" },
{ "cls": "sig", "label": "state write" },
{ "cls": "fail", "label": "reverted / blocked" }
],
"scenarios": {
"Vulnerable (push refund)": [
{ "note": "The attacker bids <b>5 ETH</b> through a contract that has <b>no receive()</b> function, becoming the highest bidder.", "hi": ["attacker", "auction"], "net": { "bid": "5 ETH (attacker)", "open": "yes" }, "chip": { "from": "attacker", "to": "auction", "label": "bid 5 ETH", "cls": "call" } },
{ "note": "An honest user calls <b>bid()</b> with <b>6 ETH</b>. To accept it, the auction must first refund the attacker's 5 ETH.", "hi": ["honest", "auction"], "chip": { "from": "honest", "to": "auction", "label": "bid 6 ETH", "cls": "call" } },
{ "note": "The auction <b>pushes</b> the 5 ETH refund to the attacker's contract inline as part of bid().", "hi": ["auction", "attacker"], "chip": { "from": "auction", "to": "attacker", "label": "refund 5 ETH", "cls": "token" } },
{ "note": "The attacker's contract has no way to receive ETH, so the refund <b>reverts</b>.", "tone": "bad", "hi": ["attacker", "auction"], "chip": { "from": "auction", "to": "attacker", "label": "revert ✋", "cls": "fail" } },
{ "note": "<b>require(success)</b> fails, so the whole bid() rolls back. The honest 6 ETH is rejected and the attacker stays highest <b>forever</b>.", "tone": "bad", "hi": ["auction"], "net": { "bid": "5 ETH (attacker)", "open": "NO: frozen" } }
],
"Fixed (pull payments)": [
{ "note": "Attacker bids <b>5 ETH</b> through the same no-receive() contract.", "hi": ["attacker", "auction"], "net": { "bid": "5 ETH (attacker)", "open": "yes" }, "chip": { "from": "attacker", "to": "auction", "label": "bid 5 ETH", "cls": "call" } },
{ "note": "Honest user bids <b>6 ETH</b>. The auction <b>credits</b> the attacker's 5 ETH to a pendingReturns mapping with <b>no external call</b>.", "tone": "ok", "hi": ["auction"], "chip": { "from": "auction", "to": "auction", "label": "credit 5 ETH", "cls": "sig" } },
{ "note": "The honest bid is accepted. They are now the highest bidder at 6 ETH; the auction keeps running.", "tone": "ok", "hi": ["honest", "auction"], "net": { "bid": "6 ETH (honest)", "open": "yes" }, "chip": { "from": "honest", "to": "auction", "label": "bid 6 ETH ✓", "cls": "token" } },
{ "note": "The attacker can <b>withdraw()</b> their 5 ETH whenever they like. If their own withdrawal reverts, <b>only they</b> are blocked; no one else is affected.", "tone": "ok", "hi": ["attacker", "auction"], "chip": { "from": "attacker", "to": "auction", "label": "withdraw()", "cls": "call" } }
]
}
}
DOS vulnerable code example
Let's examine the classic vulnerability pattern that caused the $34 million Akutars disaster.
Warning: This contract is intentionally vulnerable. Never use this pattern in production.
The vulnerable auction contract
// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
contract VulnerableAuction {
address public highestBidder;
uint256 public highestBid;
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
// Store previous bidder info before updating
uint256 refundAmount = highestBid;
address previousBidder = highestBidder;
// Update to new highest bidder
highestBidder = msg.sender;
highestBid = msg.value;
// VULNERABILITY: Push refund to previous bidder
// If this fails, the ENTIRE transaction reverts
(bool success, ) = payable(previousBidder).call{
value: refundAmount
}("");
require(success, "Refund failed"); // <-- DOS point
}
}
Why is this vulnerable?
The problem is the push payment pattern:
-
The contract pushes a refund to the previous bidder as part of
bid() -
If the previous bidder is a contract that reverts on receiving ETH, the refund fails
-
The
require(success)causes the entire transaction to revert -
No one can ever place a new bid - the auction is permanently frozen
This exact pattern caused the Akutars NFT disaster in April 2022, permanently locking 11,539 ETH ($34 million).
DOS attacker contract example
Here's how an attacker permanently freezes the vulnerable auction.
// ATTACKER CONTRACT - Educational purposes only
contract AuctionDOS {
VulnerableAuction public auction;
constructor(address _auction) {
auction = VulnerableAuction(_auction);
}
// Step 1: Place a bid through this contract
function attack() external payable {
require(msg.value > auction.highestBid(), "Need higher bid");
auction.bid{value: msg.value}();
}
// NO receive() or fallback() function!
// Any ETH sent to this contract will REVERT
// This permanently blocks the auction
}
How the attack works
-
Deploy
AuctionDOSwith the target auction address -
Call
attack()with enough ETH to become highest bidder -
That is all. The auction is permanently frozen. Any future bid attempts will try to refund this contract, fail, and revert.
The attacker contract is simple: the absence of a receive() function causes the refund to revert. The attack cost is the gas for two transactions plus the bid amount, which remains the highest bid while later bids fail.
Alternative attacker: explicit revert
// ATTACKER CONTRACT - Explicit revert version
contract ExplicitDOS {
function attack(address auction) external payable {
VulnerableAuction(auction).bid{value: msg.value}();
}
// Explicitly reject all incoming ETH
receive() external payable {
revert("I refuse all refunds");
}
}
Both approaches achieve the same result. The first is simpler; the second is more explicit about the attacker's intent.
How to prevent DOS attacks
Every defense below follows from one assumption: an external call can fail, and the failure is the caller's problem. They are ordered by how much of the attack surface each one removes.
1. The pull-over-push (withdrawal) pattern
This is the single most important defense against DOS attacks. Instead of pushing payments to users, let them pull (withdraw) independently.
The principle: never combine refund logic with business logic. Record debts in a mapping, and let each user withdraw on their own.
2. Bounded loop operations
Never iterate over unbounded arrays. Use pagination, mappings, or fixed-size data structures to keep gas costs predictable.
3. Graceful failure handling
Don't let a single external call failure kill your entire function. Log failures and continue processing, or use try-catch for non-critical operations.
Prevention effectiveness comparison
What it does: Instead of pushing refunds inline, records owed amounts in a mapping. Each user withdraws independently.
When to use: Any contract that distributes ETH or tokens to multiple recipients - auctions, crowdfunding, reward distributions, revenue sharing.
Limitation: Requires users to send an extra transaction to withdraw, adding UX friction. Over 10% of users may never claim their funds.
What it does: Replaces O(n) array lookups with O(1) mapping-based direct access, eliminating unbounded loops entirely.
When to use: Tracking balances, ownership, approvals, stakes - any per-address data that might otherwise require array iteration.
Limitation: Mappings cannot be iterated natively. If you need enumeration, use OpenZeppelin's EnumerableSet or maintain a parallel bounded array.
What it does: Splits large operations across multiple transactions using start/end indices, keeping each call within gas limits.
When to use: When you must process multiple entries - airdrops, liquidations, migrations, epoch calculations.
Limitation: Intermediate states between batches may be exploitable. Requires careful cursor tracking and double-processing prevention.
What it does: Allows an authorized account (owner, multisig, DAO) to halt contract functions during an active attack or vulnerability disclosure.
When to use: Every production DeFi contract should have some form of emergency stop. Essential for protocols managing user funds.
Limitation: Introduces centralization risk. If the pause key is compromised, the attacker can freeze the protocol indefinitely - itself a DOS attack.

DOS secure code example
The following auction isolates refund failures by recording pending returns. Production use still requires testing, access review, and protocol-specific limits.
// DEFENSIVE EXAMPLE - adapt and audit before production use
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureAuction is ReentrancyGuard, Pausable, Ownable {
address public highestBidder;
uint256 public highestBid;
bool public ended;
// PULL PATTERN: each user's refund tracked separately
mapping(address => uint256) public pendingReturns;
event NewHighBid(address indexed bidder, uint256 amount);
event AuctionEnded(address winner, uint256 amount);
event Withdrawal(address indexed bidder, uint256 amount);
// STEP 1: CHECKS - validate bid
// STEP 2: EFFECTS - update state (no external calls!)
function bid() external payable whenNotPaused {
require(!ended, "Auction ended");
require(msg.value > highestBid, "Bid too low");
// Credit previous bidder - NO external call here
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit NewHighBid(msg.sender, msg.value);
}
// STEP 3: INTERACTIONS - users withdraw independently
function withdraw() external nonReentrant {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0, "Nothing to withdraw");
// Zero before transfer (CEI pattern)
pendingReturns[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
if (!success) {
// Re-credit on failure so user can retry
pendingReturns[msg.sender] = amount;
}
emit Withdrawal(msg.sender, amount);
}
function endAuction() external onlyOwner {
ended = true;
emit AuctionEnded(highestBidder, highestBid);
}
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
}
Security features at a glance
| Feature | Protection |
|---|---|
pendingReturns mapping |
Pull pattern - no inline refunds |
nonReentrant modifier |
Prevents recursive withdrawal calls |
| State update before call | CEI pattern in withdraw() |
| Re-credit on failure | Users can retry failed withdrawals |
whenNotPaused modifier |
Emergency stop capability |
| Events on all actions | Transparency and monitoring |
Because no bid ever makes an external call, one malicious bidder cannot affect anyone else's ability to bid or withdraw. That is the property the Akutars auction and King of the Ether both lacked.
Advanced DOS patterns in DeFi
Beyond the classic auction DOS, modern DeFi protocols face sophisticated denial of service vectors.
1. Unbounded loop DOS (block gas limit)
When a function iterates over a dynamically growing array, gas cost grows linearly. Once the required gas exceeds the block gas limit (~30 million on Ethereum), the function becomes permanently uncallable.
// VULNERABLE: gas cost grows with each new depositor
function distributeRewards() external {
for (uint256 i = 0; i < depositors.length; i++) {
// At ~4,400 gas per iteration, ~6,800 depositors
// exceeds 30M block gas limit
payable(depositors[i]).transfer(rewards[i]);
}
}
The GovernMental Ponzi scheme on Ethereum lost 1,100 ETH exactly this way: its participant array grew until the payout function permanently exceeded the block gas limit.

2. Gas griefing (the 63/64 rule)
Due to the EVM's 63/64 gas forwarding rule, only 63/64 of remaining gas is forwarded to internal call() operations. An attacker can supply just enough gas for the outer function to succeed while the inner call fails silently.
// VULNERABLE: marks as executed even if sub-call fails
function forward(address target, bytes calldata data, bytes32 txHash) external {
require(!executed[txHash], "Already executed");
executed[txHash] = true; // State updated BEFORE call
(bool success, ) = target.call(data);
// success not checked! Transaction censored permanently
}
This is especially dangerous in meta-transaction relayers and multisig wallets - the transaction is marked "executed" but never actually runs.
3. Force-send ETH via selfdestruct
selfdestruct(address) forces Ether to a target contract without triggering its receive or fallback function. This can break contracts that rely on address(this).balance for logic.
// VULNERABLE: relies on address(this).balance
function deposit() external payable {
require(address(this).balance <= 7 ether, "Game over");
if (address(this).balance == 7 ether) {
winner = msg.sender; // Can never trigger if balance is forced past 7
}
}
Fix: always track balances in state variables instead of reading address(this).balance.
Comparing DOS attack surfaces
Unexpected Revert DOS
A single malicious contract in a payment loop permanently freezes the entire function. Caused the $34M Akutars disaster and the King of the Ether lockout. Fix: pull-over-push pattern.
Gas Limit DOS
Unbounded arrays grow until iteration exceeds the block gas limit. Insidious because it works fine with 10 users but breaks at 10,000. Fix: mappings, pagination, bounded arrays.
Block Stuffing DOS
Filling blocks with spam to prevent time-sensitive transactions. Expensive to execute and only works against countdown-based contracts. Fix: avoid single-block time dependencies.
Common misconceptions
"DOS only affects ETH transfer functions."
Tap to revealDOS attacks target far more than ETH transfers. Unbounded loops over growing arrays, storage bloating, governance manipulation, oracle freezes, and block stuffing all cause denial of service without involving any ETH transfer.
"DOS is not serious because it doesn't steal funds."
Tap to revealFrozen funds can impose losses even when no attacker receives them. DOS can also block liquidations, creating insolvency risk and downstream losses.
"Using transfer() instead of call() prevents DOS."
Tap to revealConsenSys published "Stop Using Solidity's transfer() Now" - the 2300 gas stipend breaks legitimate proxy wallets and multisig contracts like Gnosis Safe. After EIP-1884, transfer() can actually cause DOS by preventing smart contract wallets from receiving ETH.
"If my contract is audited, it's safe from DOS."
Tap to revealAudits are snapshots in time. A contract audited with 100 users may DOS at 10,000 due to unbounded loops. Future EVM gas repricing can also break previously safe operations. Continuous monitoring and architectural awareness are essential complements to audits.
Related vulnerabilities
DOS attacks don't exist in isolation - they frequently overlap with other smart contract vulnerability classes.
Reentrancy attacks and DOS share a common thread: both exploit the dangers of external calls. While reentrancy drains funds through recursive callbacks, the same external call mechanism enables DOS when a malicious recipient reverts. The Checks-Effects-Interactions pattern that prevents reentrancy also helps prevent certain DOS vectors by ensuring state is updated before any external interaction.
Flash loan attacks can provide temporary capital for economic DOS or rapid state growth. Fomo3D is an earlier example of using transaction economics to block time-sensitive actions.
Access control attacks directly overlap with DOS. The Parity Wallet freeze - the single largest DOS incident in history at $280M - was fundamentally an access control failure: an unprotected initWallet() function allowed an unauthorized user to call selfdestruct, permanently bricking 587 wallets.
Test your DOS attack knowledge
5 questions on unbounded loops, push payments, and gas griefing
Frequently asked questions about DOS attacks
A DOS attack on a smart contract makes it permanently unusable - like jamming a lock so no one can open the door. The contract's funds are still there on the blockchain, visible to everyone, but no one can access them. Unlike traditional web DOS attacks that flood servers with traffic, smart contract DOS attacks exploit code logic flaws where a single malicious transaction can freeze the entire contract forever.
A DOS attack targets a specific smart contract's logic - one attacker exploiting a code vulnerability to freeze operations. A DDOS (Distributed Denial of Service) attack targets the network layer - many machines flooding the blockchain with traffic to slow it down. In December 2025, Solana survived a 6 Tbps DDOS attack (the 4th largest ever recorded) without meaningful disruption. Smart contract DOS attacks are far more dangerous because they can cause permanent damage, while network-level DDOS effects are temporary.
Yes. The Parity Wallet freeze left approximately $280 million in ETH inaccessible across 587 wallets in November 2017. Without an upgrade or recovery mechanism, a contract cannot be restarted or repaired like a conventional service.
Gas griefing (SWC-126) exploits the EVM's 63/64 gas forwarding rule. When a contract makes an external call(), only 63/64 of remaining gas is forwarded. An attacker supplies just enough gas for the outer function to succeed (updating state like "transaction executed") while the inner sub-call fails silently due to gas starvation. This is especially dangerous in meta-transaction relayers and multisig wallets where transactions get permanently censored.
The pull-over-push (withdrawal) pattern separates payment recording from payment execution. Instead of the contract pushing refunds to users inline (where one failure blocks everyone), it records debts in a mapping and lets each user pull (withdraw) their funds independently. One malicious or failing recipient cannot affect any other user. This is the industry-standard defense recommended by the Solidity documentation and used by major protocols like Uniswap and Aave.
Use gas profiling tools in your testing framework (Foundry's forge test --gas-report or Hardhat's gas reporter plugin) to track how gas costs scale with input size. Write fuzz tests that progressively increase array lengths and user counts. Check that all functions with loops have bounded gas costs. Manually review every require(success) after external calls to ensure a single failure cannot block the entire function. Automated tools like Slither and Mythril also detect common DOS patterns (SWC-113 and SWC-128).
Quick reference: DOS prevention checklist
Before deploying any contract that interacts with external addresses:
-
Use pull-over-push pattern - never push payments inline with business logic
-
Eliminate unbounded loops - use mappings, pagination, or fixed-size arrays
-
Don't require success on non-critical external calls - log and continue
-
Track balances in state variables - never rely on
address(this).balance -
Validate return values before updating state in relayer/forwarding patterns; unchecked results can become silent accounting bugs
-
Implement emergency pause (OpenZeppelin Pausable) with multi-sig control
-
Set array size limits with
require(array.length < MAX_SIZE) -
Test gas scaling - verify functions work with 10x and 100x expected users
-
Get professional audits before mainnet deployment
-
Monitor post-launch for growing gas costs in critical functions
The pattern behind all three incidents
Parity ($280 million), Akutars ($34 million on launch day) and GovernMental (1,100 ETH) failed in three different ways, but each one had a single operation that every user depended on and that any one participant could make fail.
That is the thing to look for in review: find the function that must succeed for anyone to get paid, then ask who can make it revert or run out of gas. Splitting payments into per-user withdrawals, bounding iteration, and leaving a recovery path all serve that one question.
Practice DOS review
The Smart Contract Hacking course includes hands-on exercises covering failed external calls, gas limits, and recovery design.
Sources and editorial notes
Reviewed by JohnnyTime. Last updated .
Master DOS Attacks in a safe lab
Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.