Dos Attacks
Smart Contract Vulnerability Deep Dive
Denial of Service (DOS) Attacks in Smart Contracts: How They Work and How to Prevent Them
Denial of Service (DOS) attacks are the silent assassins of the Web3 world. Forget the flashy, split-second million-dollar heists - DOS vulnerabilities do something much worse. They permanently freeze funds. We're talking millions of dollars locked away with absolutely zero way to recover them. Ever. On an immutable blockchain, when a contract freezes, it stays frozen for eternity.
In this ultimate guide, we’ll walk you through everything you need to know about DOS attacks in smart contracts. From the legendary Fomo3D block stuffing heist to the heartbreaking $34 million Akutars NFT disaster, we'll break it all down. Plus, we'll dive into hands-on code examples and give you battle-tested, production-ready prevention patterns to keep your protocols safe.
What Is a Denial of Service (DOS) Attack in Web3?
A Denial of Service (DOS) attack in a smart contract happens when a malicious actor exploits a vulnerability to render the contract completely unusable. This means blocked withdrawals, indefinitely frozen funds, and critical operations grinding to a halt.
Unlike traditional Web2 DOS attacks that just overwhelm web servers with bot traffic, smart contract DOS attacks are smarter. They exploit flaws in the code's logic. All it takes is one perfectly crafted, malicious transaction to completely brick an entire protocol. Forever.
The Jammed Door Analogy
Think of a high-security bank vault with a fancy automated revolving door. Customers line up, walk in one at a time, grab their cash, and the door spins for the next person. Smooth, right?
Now, imagine a saboteur aggressively jamming a massive steel beam straight into the door's gears. The vault is completely untouched. The money is sitting right there in the safes. The locks work. But there's a huge problem: nobody can get through the door.
That’s exactly how a smart contract DOS attack works. The funds are perfectly safe. The contract logic is technically "functioning." But no one can access their crypto because one tiny, critical mechanism is completely jammed.
Why DOS Attacks Are So Damn Dangerous
DOS vulnerabilities aren't just annoying delays. They have caused some of the most catastrophic, permanent, and heartbreaking losses in crypto history:
| 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) |
Here is what makes a Web3 DOS attack uniquely terrifying: its permanence. On an immutable blockchain, frozen funds stay frozen. There is absolutely no admin you can email, no shiny server you can reboot, and no convenient database to roll back. If your code freezes, the money is gone.
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.
🔥 Ready to try exploiting a DOS vulnerability yourself in a safe, legal environment? The Smart Contract Hacking course sets you up with hands-on lab exercises where you’ll learn to identify, exploit, and patch these exact DOS vulnerabilities - just like a world-class security researcher.
The Legendary Fomo3D Heist: A $3 Million Masterclass in DOS 💥
Fomo3D wasn't just another decentralized application - it was a brutal, incredibly clever $3 million masterclass teaching us exactly how Ethereum's core block mechanics could be weaponized into a DOS attack.
Wait, What Was Fomo3D?
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:
Here’s how the game worked:
-
Players bought "keys" with ETH, essentially beefing up a massive 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 creators theorized the pot would grow forever. Human greed, after all, meant someone would always buy another key just to steal that massive jackpot at the last second.
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), each consuming approximately 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 genius behind this attack spent an estimated 40-50 ETH (about $11,000 to $13,800) just on gas fees to absolutely dominate those blocks. The payout? 10,469 ETH ($3 million). That's a mind-blowing 250x return on investment in a matter of minutes.
The wildest part? The Fomo3D developer publicly stated the attacker's strategy was actually "part of the game." The Web3 community was heavily divided. Some purists saw it as legitimate, hyper-rational gameplay within a system perfectly designed around raw greed. Others called it a devastating, clear-cut exploit.
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
Understanding the attack mechanism is crucial for prevention. Let's break down the most common vector -- DOS with unexpected revert.
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 just <b>credits</b> the attacker's 5 ETH to a pendingReturns mapping — <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 stuck — 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.
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 -
Done. The auction is permanently frozen. Any future bid attempts will try to refund this contract, fail, and revert.
The attacker contract is brutally simple -- it's what it doesn't have (no receive() function) that creates the vulnerability. The total attack cost is just the gas for two transactions plus the bid amount (which the attacker effectively gets to keep as no one can outbid them).
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.
Ready to write exploits like this yourself? The Smart Contract Hacking course covers DOS attacks in-depth with real-world scenarios. Learn from JohnnyTime (12+ years in cybersecurity) and Trust (#1 Code4rena warden).
How to Prevent DOS Attacks: Best Practices
Preventing DOS attacks requires understanding that external calls are inherently unreliable. Here are the industry-standard techniques ranked by effectiveness.
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
Here's a production-ready auction that is immune to all DOS attack vectors described above.
// SECURE CONTRACT - Production-ready
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 |
This implementation would have prevented the $34 million Akutars freeze and the King of the Ether attack. A single malicious bidder cannot affect any other user's ability to bid or withdraw.
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 when its participant array grew so large that 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 silently fails.
// 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/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.
These advanced DOS patterns separate junior auditors from senior researchers. The Smart Contract Hacking course covers DOS attacks alongside flash loans, oracle manipulation, and more. Join 2,000+ security researchers in our Discord community.
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 revealPermanently frozen funds are just as devastating as stolen funds -- ask the 587 Parity wallet owners whose $280M has been inaccessible since 2017. DOS can also block critical liquidations, causing protocol insolvency and cascading losses across DeFi.
"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 amplify DOS by providing massive capital for block stuffing (paying extreme gas fees to fill blocks) or inflating arrays to unbounded sizes in a single atomic transaction. The Fomo3D block stuffing attack is a precursor to modern flash-loan-enabled manipulation.
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 -- How well do you really know this attack?
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, absolutely. This is what makes smart contract DOS attacks uniquely devastating. The Parity Wallet freeze locked $280 million in ETH across 587 wallets in November 2017 -- those funds remain frozen to this day and will remain frozen forever. On an immutable blockchain, there is no admin to call, no server to restart, and no database to rollback. Once frozen, always frozen.
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
Conclusion: The Invisible Attack That Locks Funds Forever
Sure, DOS attacks might not always grab the flashy "$100 million stolen!" headlines that flash loan hacks do. But make no mistake, they are uniquely devastating:
-
The Parity Wallet freeze locked away $280 million that will never, ever be recovered.
-
The Akutars NFT disaster permanently froze $34 million on the very day it launched.
-
The GovernMental Ponzi proved that left unchecked, unbounded arrays can brick contracts for eternity.
The good news? Smart contract DOS attacks are 100% preventable.
If you rely on the battle-tested pull-over-push pattern for all payments, ruthlessly eliminate unbounded loops, and never let a single external call failure block your core contract logic, you'll be lightyears ahead. Applying just these three golden rules would have prevented almost every major DOS incident in crypto history.
Don't let your protocol become the next multi-million dollar cautionary tale.
Take Your Web3 Security Skills to the Next Level
Understanding denial of service attacks is just the beginning. To become a professional smart contract auditor, you need hands-on experience with real exploit scenarios.
The Smart Contract Hacking course delivers:
-
320+ videos covering DOS, flash loans, reentrancy, oracle manipulation, and more
-
40+ hands-on exercises exploiting and securing real contracts
-
Expert instruction from JohnnyTime, Trust (#1 Code4rena warden), and Pashov
-
2,000+ member Discord for support and job opportunities
-
SSCH Certification to validate your expertise
Our students win audit contests, land security jobs, and earn significant bug bounties. See their success stories.
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.