Solidity DoS and Griefing Defense
Solidity DoS and griefing defense is the practice of keeping a contract usable when one caller tries to block it or make it too costly for others. A blocked function helps nobody, so the design must bound the work and let each person act alone.
A bounded list always finishes
PayList caps its list at fifty recipients. Three recipients are paid in one short loop, and the fifty-first add reverts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract PayList {
address[] public recipients;
function addRecipient(address person) public {
require(recipients.length < 50, "list full");
recipients.push(person);
}
function payAll() public payable {
for (uint256 i = 0; i < recipients.length; i++) {
(bool success, ) = payable(recipients[i]).call{value: 1 ether}("");
require(success, "send failed");
}
}
}
One rejecting recipient must not block the rest
PushVault pays everyone in one call, so one rejecting recipient reverts the whole distribution. PullVault lets each person claim alone, so one person's share never blocks another.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract Rejector {
}
contract PushVault {
address[] public recipients;
mapping(address => uint256) public shares;
function addRecipient(address person) public {
recipients.push(person);
}
function setShare(address person, uint256 amount) public {
shares[person] = amount;
}
function distribute() public {
for (uint256 i = 0; i < recipients.length; i++) {
(bool success, ) = payable(recipients[i]).call{value: shares[recipients[i]]}("");
require(success, "send failed");
}
}
}
contract PullVault {
mapping(address => uint256) public shares;
function setShare(address person, uint256 amount) public {
shares[person] = amount;
}
function claim() public {
uint256 amount = shares[msg.sender];
require(amount > 0, "nothing to claim");
shares[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "send failed");
}
}
Large lists are paid in small pages
PagedPay pays at most two recipients per call. Four recipients are paid in two pages, and a page of three reverts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract PagedPay {
address[] public recipients;
mapping(address => uint256) public shares;
function addRecipient(address person) public {
recipients.push(person);
}
function setShare(address person, uint256 amount) public {
shares[person] = amount;
}
function payRange(uint256 start, uint256 end) public {
require(end <= recipients.length, "past end");
require(end - start <= 2, "chunk too big");
for (uint256 i = start; i < end; i++) {
(bool success, ) = payable(recipients[i]).call{value: shares[recipients[i]]}("");
require(success, "send failed");
}
}
}
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract Rejector {
}
contract Distributor {
address public owner;
address[] public recipients;
mapping(address => uint256) public shares;
constructor() {
owner = msg.sender;
}
function setShare(address person, uint256 amount) public {
require(msg.sender == owner, "not owner");
require(recipients.length < 50, "list full");
bool found = false;
for (uint256 i = 0; i < recipients.length; i++) {
if (recipients[i] == person) {
found = true;
}
}
if (!found) {
recipients.push(person);
}
shares[person] = amount;
}
function claim() public {
uint256 amount = shares[msg.sender];
require(amount > 0, "nothing to claim");
shares[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "send failed");
}
}
Key points
- Unbounded loop: An unbounded loop is a loop over a list that can grow without a limit.
- Push payment: A push payment is one call that pays many people at once.
- Pull payment: A pull payment lets each person claim their own share in a separate call.
- Griefing: Griefing is an action that costs others time or money without gaining the attacker much.
- Bounded work: Bounded work is a loop or a page with a fixed maximum size.
In this example, Distributor caps its recipient list at fifty and lets only the owner set shares. claim is a pull payment that zeroes the share before sending it. Two users claim their exact shares, and the rejecting recipient's share stays in the vault instead of blocking anyone.
Note
A function that pays everyone at once fails for everyone when one recipient rejects the payment.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A distribution pays fifty people in one call, and one recipient rejects ether. What happens to the other forty-nine payments?