Denial of Service Exploit Generator
Sigvex exploit generator that validates DoS vulnerabilities by simulating unbounded loop execution and external call blocking scenarios to confirm contracts can be permanently disabled.
Denial of Service Exploit Generator
Overview
The denial-of-service exploit generator validates findings from the dos, denial_of_service, unbounded_loop, and gas_limit detectors by executing the target contract under two distinct scenarios: one with a small data set that executes normally, and one with a large data set or a reverting external recipient that causes the function to permanently fail. The generator confirms whether the vulnerability makes contract functions permanently unusable.
Denial-of-service exploits have caused significant damage across early Ethereum contracts. GovernMental was permanently locked due to an unbounded loop, and the King of the Ether contract blocked legitimate refunds when recipients refused ETH, demonstrating both primary DoS attack vectors.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Unbounded loop (array inflation):
- A contract distributes ETH to an array of
recipientsin a single call todistributeTokens(). - The attacker repeatedly calls
addRecipient()to inflate the array to thousands of entries. distributeTokens()now iterates over too many entries and runs out of gas every time it is called.- The function is permanently unusable. Funds deposited for distribution are locked forever.
External call blocking (push-over-pull):
- A contract refund function calls
payable(user).transfer(amount)after zeroing the user’s balance. - The attacker deploys a contract whose
receive()always reverts. - When
refund(attackerContract)is called, the external transfer fails and reverts the entire transaction. - The attacker’s balance is zeroed but they receive nothing. If the same contract is the last gating refund condition, the entire protocol becomes unresponsive.
Exploit Mechanics
The generator dispatches four scenarios with storage slot 0 encoding array size and slot 2 encoding payment pattern:
| Scenario | Array size (slot 0) | Gas limit check (slot 1) | Pull pattern (slot 2) | Description |
|---|---|---|---|---|
| 1 — Small array | 10 | 0 (no) | 0 (push) | Baseline: succeeds |
| 2 — Large array | 10,000 | 0 (no) | 0 (push) | Gas exhaustion via inflation |
| 3 — Reverting recipient | N/A | 0 (no) | 1 (push) | External call blocks execution |
| 4 — Protected | 10,000 | 1 (yes) | 1 (pull) | Gas checks + pull pattern |
The fallback selector 0xe4fc6b6d (distribute) is used when no specific selector is extracted from the finding location.
Verdict:
- Scenario 1 succeeds and Scenario 2 reverts or exhausts >9,000,000 gas →
unbounded_loopconfirmed (confidence 0.85). - Scenario 1 succeeds and Scenario 3 reverts →
external_call_blockingconfirmed (confidence 0.85).
The generated PoC demonstrates all four DoS attack vectors: unbounded loop via VulnerableAirdrop, external call blocking via RefundAttacker, secure pull-over-push via SecureRefund, and paginated distribution via SecureAirdrop.
// ATTACKER: Array inflation DoS
contract AirdropAttacker {
VulnerableAirdrop public airdrop;
function attack() external {
// Add 1000 recipients to make loop unbounded
for (uint256 i = 0; i < 1000; i++) {
airdrop.addRecipient(address(uint160(i)), 1 ether);
}
// Now distributeTokens() always runs out of gas
}
}
// ATTACKER: Refuse payment to block refunds
contract RefundAttacker {
receive() external payable {
revert("I refuse payment!");
}
// refund(address(this)) will always revert
}
Remediation
- Detector: DoS Detector
- Remediation Guide: DoS Remediation
Replace push-over-pull patterns with pull withdrawals, and paginate loops with a maximum batch size:
// SECURE: Pull pattern — user withdraws their own funds
contract SecureRefund {
mapping(address => uint256) public withdrawable;
function withdraw() external {
uint256 amount = withdrawable[msg.sender];
require(amount > 0, "No balance");
withdrawable[msg.sender] = 0;
(bool success,) = msg.sender.call{value: amount}("");
if (!success) {
withdrawable[msg.sender] = amount;
revert("Transfer failed");
}
}
}
// SECURE: Bounded loop with pagination
contract SecureAirdrop {
uint256 public constant MAX_BATCH_SIZE = 50;
uint256 public lastProcessedIndex;
function distributeTokens(uint256 batchSize) external {
require(batchSize <= MAX_BATCH_SIZE, "Batch too large");
uint256 endIndex = lastProcessedIndex + batchSize;
if (endIndex > recipients.length) endIndex = recipients.length;
for (uint256 i = lastProcessedIndex; i < endIndex; i++) {
// Don't revert on individual failures
(bool success,) = payable(recipients[i]).call{value: amounts[i]}("");
if (!success) emit TransferFailed(recipients[i], amounts[i]);
}
lastProcessedIndex = endIndex;
}
}
References
- SWC-113: DoS with Failed Call
- SWC-128: DoS with Block Gas Limit
- King of the Ether Throne Post-Mortem (2016)
- ConsenSys: DoS Best Practices