Denial of Service Remediation
How to prevent DoS vulnerabilities in Solidity by replacing push-payment patterns with pull payments and bounding loop iterations.
Denial of Service Remediation
Overview
Denial of Service vulnerabilities in smart contracts result from placing external call success or unbounded iteration count on the critical path of contract execution. The remediation strategy is to eliminate this dependency: replace push payments with pull payments, and replace unbounded loops with paginated batching.
Related Detector: Denial of Service
Recommended Fix
Before (Vulnerable)
// Push-payment DoS — malicious recipient blocks all future bids
contract VulnerableAuction {
address public highestBidder;
uint256 public highestBid;
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
// If highestBidder is a contract that reverts on receive,
// no one can ever outbid — auction is permanently frozen
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
After (Fixed)
// Pull payment — each user withdraws their own refund
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)) {
// Record refund — never push
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0; // Reset before transfer (CEI)
payable(msg.sender).transfer(amount);
}
}
}
Alternative Mitigations
Paginated iteration for unbounded loops:
contract SafeDistributor {
address[] public recipients;
uint256 public nextIndex;
function distribute(uint256 batchSize) external {
uint256 end = nextIndex + batchSize;
if (end > recipients.length) end = recipients.length;
for (uint256 i = nextIndex; i < end; i++) {
// Non-reverting call — failure does not block the batch
(bool success, ) = payable(recipients[i]).call{value: 1 ether}("");
if (!success) emit DistributionFailed(recipients[i]);
}
nextIndex = end;
}
}
Gas-capped external calls (when a bounded gas budget is acceptable):
// Only appropriate when the called function is simple (no complex logic)
(bool success, ) = target.call{gas: 2300}("");
Common Mistakes
Using transfer() or send() in loops — both bubble up reverts from the recipient. Use low-level call with explicit success checking instead.
Removing the revert on failed distribution without also paginating — continuing after failure is correct, but if the loop is still unbounded, gas exhaustion remains possible.
Forgetting to bound batchSize — ensure callers cannot pass batchSize = type(uint256).max.