📋 What Is It?
Denial of Service (DoS) in a smart contract means making the contract-or a critical function within it-permanently or temporarily unusable, or locking funds so no one can withdraw them. Unlike a web DoS that ends when the traffic stops, an on-chain DoS is often permanent: immutable code has no "restart" button, so a wedged contract can trap funds forever. What makes it distinct from a plain bug is that an attacker can deliberately engineer the broken state-becoming a "poison" recipient that always reverts, or inflating an array past the gas limit-and a single actor can freeze the shared mechanism for everyone else while usually losing nothing themselves.
⚠️ Common Exploits
- Poison recipient: one reverting
receive()in a push-payment loop blocks all payouts - Refund-on-outbid freeze: a bidder that rejects ETH can never be refunded, so no one can outbid
- Unbounded loop: an array grown with user input eventually exceeds the block gas limit
- Gas griefing: a recipient burns all forwarded gas so the surrounding logic fails
- Broken dependency: a required call to a paused or self-destructed contract reverts forever
- Lost single owner: recovery gated on one key is uncallable if that key is gone
- Forced-balance jam:
selfdestructforce-sends ETH to break an exact-balance invariant
🔴 Attack Flow
↓
2. Position: become a recipient/bidder the contract MUST interact with
↓
3. Poison: reject ETH, burn all gas, inflate the array, or break the dependency
↓
4. Freeze: the critical path now reverts or runs out of gas for everyone
↓
5. IMPACT: Frozen functionality and permanently locked funds!
❌ Vulnerable Code
✅ Secure Code
✓ Prevention Checklist
- Prefer pull over push - let each user withdraw their own funds
- Bound every loop; never iterate over unbounded user-controlled data
- Use pagination with a hard per-call cap when a loop is unavoidable
- Isolate external calls - credit failures instead of reverting the batch
- Convert refund-on-outbid into a pull credit, never a pushed transfer
- Avoid hard dependencies; make them swappable and fail gracefully
- Wrap risky external calls in
try/catch - Use per-user mappings and internal accounting over arrays you must sweep
- Never trust
address(this).balance; use>=, not== - Use multisig + two-step ownership (
Ownable2Step) for recovery - Forward a bounded gas stipend, or use pull, to defeat gas griefing
- Fuzz array sizes and add gas-bound assertions for batch functions
🔍 Detection & Tools
Analysis Tools:
Secure Libraries:
How to Test:
- Deploy a poison
RevertOnReceiverecipient and assert others are still paid or credited - Fuzz array sizes to prove batch functions stay under a gas ceiling or paginate
- Add gas-bound assertions: critical functions cost a safe fraction of the block limit
- Test a gas-burning recipient that consumes all forwarded gas
- Force ETH in via
selfdestructand assert balance-based logic still works - Simulate a paused / self-destructed dependency and confirm graceful failure
- Monitor on-chain: alert if a critical function starts reverting or nears the gas ceiling
🌍 Real-World Incident Classes
- Push-payment refund DoS (auctions and "King"-style games): a contract refunds the previous holder by pushing ETH when a new participant takes the position. An attacker takes the position from a contract that rejects incoming ETH, so every future attempt must first refund that poison contract-which always fails-freezing the mechanism with the attacker permanently on top. Mechanism: a state change that depends on a payment to an untrusted recipient succeeding.
- Unbounded-loop gas DoS (growing arrays): a contract iterates over an array of investors or payouts that grows without bound. As it grows-organically or because an attacker cheaply adds many entries-the per-transaction cost eventually exceeds the block gas limit, and the batch operation can never fit in a block again. Mechanism: a critical operation whose cost scales with user-controlled input.
- Locked-funds griefing (broken dependency / lost owner): a contract depends on an external library, oracle, or wallet for a required step, or gates all recovery behind a single owner. When that shared dependency is self-destructed or paused-or the sole owner key is lost-every dependent function reverts with no alternate code path, and large balances become permanently inaccessible. Mechanism: a single hard dependency or single point of privileged control with no fallback.
- Forced-balance invariant griefing: logic that trusts
address(this).balancewith an exact-equality check can be jammed by force-sending ETH viaselfdestruct, making the equality permanently false so the function can never succeed. Mechanism: the raw balance is not fully under the contract's control.
Note: these are recurring classes of vulnerability, not claims about any single named project. Exact loss figures vary by report; the durable lesson is that DoS is common, cheap to trigger, and frequently irreversible on immutable contracts.
📌 Quick Tips
- DO NOT push ETH/tokens to many recipients in one loop
- DO NOT loop over arrays that grow with user input
- DO NOT make a new action depend on refunding an untrusted party
- DO prefer pull payments and per-user accounting
- DO bound and paginate every batch operation
- DO use multisig + two-step ownership and swappable dependencies
- DO use internal counters and
>=, never trust the raw balance