Batch Operation Atomicity
Detects batch operations that lack atomicity guarantees, allowing partial execution failures to leave the contract in an inconsistent state.
Batch Operation Atomicity
Overview
The batch operation atomicity detector identifies functions that process arrays of operations (transfers, swaps, claims) where a single failure can either revert the entire batch or silently skip items, depending on error handling. Both outcomes are problematic: full revert enables griefing attacks (one poisoned element blocks the batch), while silent skipping causes fund loss or inconsistent state.
Why This Is an Issue
Batch operations are common in airdrop contracts, multi-send utilities, and governance execution. When a batch iterates over user-supplied data and calls external contracts, any single failed call can:
- Revert everything: An attacker includes a contract that always reverts, blocking legitimate recipients.
- Skip silently: Using
try/catchor unchecked low-level calls, the batch continues but affected users never receive funds.
The SushiSwap MasterChef exploit and several airdrop contract failures stem from this pattern. Griefing cost is minimal (one reverting contract), while impact blocks potentially millions in distributions.
How to Resolve
// Before: Single failure reverts entire batch
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint i = 0; i < recipients.length; i++) {
IERC20(token).transfer(recipients[i], amounts[i]); // Reverts on failure
}
}
// After: Track failures, allow retry
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external returns (bool[] memory results) {
results = new bool[](recipients.length);
for (uint i = 0; i < recipients.length; i++) {
(bool success, ) = address(token).call(
abi.encodeCall(IERC20.transfer, (recipients[i], amounts[i]))
);
results[i] = success;
if (!success) {
emit TransferFailed(recipients[i], amounts[i]);
}
}
}
Examples
Vulnerable Code
contract VulnerableBatchSend {
function multiSend(address[] calldata to, uint256[] calldata values) external payable {
for (uint i = 0; i < to.length; i++) {
// If any recipient is a contract that reverts, entire batch fails
(bool success, ) = to[i].call{value: values[i]}("");
require(success, "Transfer failed"); // One failure blocks all
}
}
}
Fixed Code
contract SafeBatchSend {
event SendFailed(address indexed recipient, uint256 amount);
function multiSend(
address[] calldata to,
uint256[] calldata values
) external payable returns (uint256 failCount) {
for (uint i = 0; i < to.length; i++) {
(bool success, ) = to[i].call{value: values[i]}("");
if (!success) {
failCount++;
emit SendFailed(to[i], values[i]);
// Funds remain in contract for retry or refund
}
}
}
}
Sample Sigvex Output
{
"detector_id": "batch-operation-atomicity",
"severity": "high",
"confidence": 0.72,
"description": "Batch operation in function multiSend() makes 1 external call(s) inside a loop with revert-on-failure semantics. A single reverting recipient blocks the entire batch of transfers.",
"location": { "function": "multiSend(address[],uint256[])", "offset": 84 }
}
Detection Methodology
- Loop detection: Identifies loops in the CFG that iterate over array parameters.
- External call scan: Finds CALL/DELEGATECALL opcodes within loop bodies.
- Error handling analysis: Checks whether call results are consumed by REVERT paths or ignored.
- Pattern classification: Categorizes as “revert-all” (griefing risk) or “silent-skip” (fund loss risk).
Limitations
- Cannot determine whether a batch operation is intended to be atomic by design (e.g., flash loan repayment must be atomic).
- Try/catch blocks that properly handle failures and escrow funds for retry are still flagged.
- Multi-call patterns using
delegatecallto self are not analyzed for individual operation failures.
Related Detectors
- Loop Gas Exhaustion — unbounded loops that exhaust gas
- Calls In Loop — DoS from external calls inside loops
- Controlled Array Length — unbounded array iteration