Denial of Service
Detects patterns that allow attackers to permanently block contract execution, including unbounded loops, push-payment patterns, and external call failures in critical paths.
Denial of Service (DoS)
Overview
Remediation Guide: How to Fix Denial of Service
The Denial of Service detector identifies smart contract patterns that allow an attacker — or an uncooperative participant — to permanently block contract execution. In the EVM context, DoS attacks fall into three primary categories: unbounded loops that exhaust block gas limits, push-payment patterns where a failing external call blocks all future progress, and external call failures in critical paths where a reverted call prevents essential state transitions.
All three patterns share a common root: the contract places the success of an external interaction on the critical path of its own execution, giving any external party leverage to freeze the contract. Sigvex detects these by analyzing CFG loop structures, external call patterns, and the relationship between external calls and control flow.
Why This Is an Issue
A DoS vulnerability can permanently brick a contract: funds are locked, governance is paralyzed, and no upgrade path exists without a full redeployment. Unlike theft, DoS attacks destroy utility rather than redirecting value. They are particularly devastating in auction contracts, governance systems, and payment distribution contracts where even temporary unavailability causes significant harm.
The attack surface is broad: any contract that iterates over a user-controlled array, refunds ETH during a state transition, or calls an external contract in a mandatory path is potentially affected. A single malicious address in a recipients array — or a single contract that refuses ETH — can permanently freeze the system.
Historical incidents include the King of the Ether freeze (2016), where a push-payment pattern locked all funds when the incumbent “king” was a contract that rejected ETH, and GovernorBravo DoS attacks (2022), where unbounded loops over voting targets allowed griefing that disabled governance functionality.
How to Resolve
For push-payment DoS, replace the push pattern with a pull payment model: record pending refunds and allow recipients to withdraw at their own convenience. For unbounded loop DoS, batch processing with checkpoints ensures gas consumption is bounded per transaction.
// Before: Vulnerable push-payment in auction
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
// VULNERABLE: if previous bidder is a contract that reverts on ETH receipt,
// this line always fails — no one can ever outbid the current highest bidder
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBidder = msg.sender;
highestBid = msg.value;
}
// After: Fixed with pull payment
mapping(address => uint256) public pendingReturns;
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
if (highestBidder != address(0)) {
// FIXED: record the refund — do not push ETH
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
Examples
Vulnerable Code
// Pattern 1: Unbounded loop over user-controlled array
contract VulnerableAirdrop {
address[] public recipients;
function addRecipient(address recipient) external {
recipients.push(recipient); // Attacker can add thousands of addresses
}
// VULNERABLE: loop length is user-controlled
// Adding enough addresses causes this to always exceed the block gas limit
function distributeAirdrop() external {
for (uint256 i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(1 ether);
}
}
}
// Pattern 2: External call failure on critical path
contract VulnerableAuction {
address public highestBidder;
uint256 public highestBid;
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
// VULNERABLE: malicious bidder makes this call always revert
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
Fixed Code
// Fix 1: Pull payment pattern for auctions
contract SafeAuction {
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public pendingReturns;
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
}
// Fix 2: Paginated loop with checkpoint
contract SafeAirdrop {
address[] public recipients;
uint256 public lastProcessedIndex;
function distributeAirdrop(uint256 batchSize) external {
uint256 end = Math.min(lastProcessedIndex + batchSize, recipients.length);
for (uint256 i = lastProcessedIndex; i < end; i++) {
(bool success, ) = payable(recipients[i]).call{value: 1 ether}("");
if (!success) emit DistributionFailed(recipients[i]);
}
lastProcessedIndex = end;
}
}
Sample Sigvex Output
{
"detector_id": "dos",
"severity": "high",
"confidence": 0.72,
"description": "Function distributeAirdrop() contains an unbounded loop (offset 0x3c–0x8a) iterating over a user-controlled storage array (slot 0x01). An attacker can grow the array to exhaust the block gas limit.",
"location": {
"function": "distributeAirdrop()",
"offset": 60
}
}
Detection Methodology
Sigvex detects DoS patterns through CFG-level analysis of the decompiled bytecode:
- Unbounded loop detection: Identifies loop structures (back-edges in the CFG) where the loop bound is derived from a
SLOADof a storage array length. Checks whether the length slot is writable by external callers. - Push-payment detection: Identifies
CALLorTRANSFERopcodes inside control flow that is on the mandatory path to a state update — specifically, where the external call’s success is a required condition for a subsequentSSTORE. - External call in loop: Identifies
CALLorDELEGATECALLinstructions inside loop bodies, which allow external contracts to consume arbitrary gas per iteration.
Confidence is Medium because distinguishing between a trusted and untrusted loop target requires inter-procedural analysis that may not be available from bytecode alone.
Limitations
False positives:
- Loops over fixed-size arrays declared in contract storage are flagged even when the array cannot grow beyond a practical size.
- External calls to well-known contracts (e.g., WETH, canonical token contracts) may be flagged even though they do not revert on ETH receipt.
False negatives:
- DoS via gas griefing through delegated calls with uncapped gas is not always detected when the call target is not statically known.
- DoS through storage exhaustion (writing large amounts of data to push contracts toward storage limits) is not currently detected.
Related Detectors
- Unchecked Call — detects calls whose return values are not checked, which can create silent DoS conditions
- Reentrancy — external calls in loops can also be reentrancy vectors