Skip to main content
Sigvex

Calls in Loop Remediation

How to stop one failing recipient from blocking everyone by removing external calls from loops with the pull-payment pattern.

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).

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/catch and 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.

References