DoS with Failed Call
Detects denial of service vulnerabilities where external calls in loops lack error handling, allowing one failed call to block entire batch operations.
DoS with Failed Call
Overview
The DoS with failed call detector identifies loops containing external calls (CALL, STATICCALL, DELEGATECALL) that do not check the return value or handle failures gracefully. When a loop iterates over recipients and any single call reverts, the entire transaction fails — blocking all remaining transfers in the batch.
This pattern was first exploited in the King of the Ether throne contract (2016) and continues to affect batch distribution, airdrop, and payout functions.
Why This Is an Issue
Batch operations that distribute funds to multiple addresses are common in DeFi. If these operations use a loop with unchecked external calls, any recipient can block the entire batch:
- A malicious contract can revert on receive, preventing all other recipients from getting paid.
- A self-destructed contract causes the call to fail, permanently locking funds in the contract.
- A contract with a gas-expensive fallback can consume all remaining gas.
The result is permanent denial of service: the function cannot complete, and the funds remain stuck.
How to Resolve
// Before: Vulnerable -- one failed call blocks all
function distribute(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint256 i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(amounts[i]); // Reverts on failure
}
}
// After: Fixed -- handle failures individually
function distribute(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint256 i = 0; i < recipients.length; i++) {
(bool success, ) = payable(recipients[i]).call{value: amounts[i]}("");
if (!success) {
emit TransferFailed(recipients[i], amounts[i]);
}
}
}
Examples
Vulnerable
function payWinners(address[] memory winners) external {
for (uint256 i = 0; i < winners.length; i++) {
// If any winner is a contract that reverts, nobody gets paid
(bool ok, ) = winners[i].call{value: prizes[i]}("");
require(ok, "Transfer failed"); // Blocks entire batch
}
}
Fixed (Pull Pattern)
mapping(address => uint256) public pendingWithdrawals;
function recordWinnings(address[] memory winners, uint256[] memory amounts) external onlyOwner {
for (uint256 i = 0; i < winners.length; i++) {
pendingWithdrawals[winners[i]] += amounts[i];
}
}
function withdraw() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "Nothing to withdraw");
pendingWithdrawals[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
}
Sample Sigvex Output
[HIGH] dos-failed-call
DoS via Failed Call in loop - function distribute
Location: distribute @ block 1, instruction 2
Confidence: 0.75
Function 'distribute' makes an external call inside a loop at block 1
WITHOUT checking the return value or handling failures.
Detection Methodology
- Loop detection: Identifies back-edges in control flow (blocks that jump to earlier blocks).
- Call-in-loop identification: Finds
CALL,STATICCALL, andDELEGATECALLinstructions within loop body blocks. - Error handling check: Looks for
ISZERO(return value check) followed by a conditional jump after the call. Logs after a call also indicate error handling. - Proxy filtering: Skips proxy fallback and dispatch functions where delegation forwarding is not a batch loop.
- Access control adjustment: Findings in access-controlled contracts receive reduced severity.
Limitations
False positives: Loops that handle errors through try/catch blocks in a separate basic block may be flagged if the detector does not trace the error path across blocks. False negatives: Error handling performed in a called helper function (e.g., _safeTransfer) rather than inline after the call is not detected.
Related Detectors
- DoS — detects general denial of service patterns
- Loop Gas Exhaustion — detects unbounded loop gas consumption
- Unchecked Call — detects unchecked return values outside loops