Calls in Loop Remediation
Overview
Related Detector: Calls in Loop
When a function loops over recipients and makes an external call to each, one failing call reverts the whole transaction. A single participant — a contract that throws, a blacklisted token address, a recipient that runs out of gas — can then block an operation for everyone. The remediation is to move value out of the loop and let each recipient claim independently (the pull-payment pattern).
Recommended Fix
Before (Vulnerable)
function payout(address[] calldata users, uint256 amount) external {
for (uint256 i; i < users.length; ++i) {
// If one recipient reverts, no one gets paid.
(bool ok, ) = users[i].call{value: amount}("");
require(ok, "Transfer failed");
}
}
After (Fixed — pull payments)
mapping(address => uint256) public owed;
function allocate(address[] calldata users, uint256 amount) external {
for (uint256 i; i < users.length; ++i) {
owed[users[i]] += amount; // bookkeeping only, no external calls
}
}
function claim() external {
uint256 amount = owed[msg.sender];
owed[msg.sender] = 0; // effect before interaction
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
}
Each recipient’s failure is now isolated to their own claim() and cannot affect anyone else.
Common Mistakes
- Wrapping the in-loop call in a
try/catchand continuing, which mitigates the revert but still lets a griefer consume unbounded gas per iteration. - Bounding the loop length but keeping the external call inside it — the DoS shrinks but does not disappear.
- Forgetting to zero the owed balance before the transfer in
claim(), reopening a reentrancy window.