You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Critical contract logic depends on an external call succeeding
A single revert in the external call blocks the entire function
OR: strict equality checks on contract balance can be violated by force-sent ETH
OR: division by zero is possible due to unvalidated denominators
Vulnerable Pattern
// Push-payment: one reverting recipient blocks all paymentsfunction payAll() external {
for (uint256 i =0; i < recipients.length; i++) {
// If ANY recipient reverts (e.g., contract with no receive()),// the entire function reverts — no one gets paidrequire(payable(recipients[i]).send(amounts[i]), "transfer failed");
}
}
// Strict balance check broken by force-sent ETHfunction withdraw() external {
// Attacker sends ETH via selfdestruct, breaking this checkrequire(address(this).balance == expectedBalance, "invariant");
_processWithdrawal();
}
// Division by zerofunction distribute(uint256totalShares) external {
// If totalShares == 0, this reverts and blocks the functionuint256 perShare = totalRewards / totalShares;
}
Detection Heuristics
Search for loops containing require or assert on external call results — one failure blocks all iterations
Search for push-payment patterns: contract iterating over recipients and sending ETH/tokens in one transaction
Search for strict balance equality checks (address(this).balance ==) — these can be broken by selfdestruct or coinbase rewards force-sending ETH
Search for division operations and check if the denominator can be zero
Check for require(success) after .send() or .call() inside loops — this turns a single recipient failure into a full DoS
Look for "highest bidder" or "king of the hill" patterns where the current leader's refund must succeed for a new leader to be set
False Positives
Pull-payment pattern is used (each recipient withdraws individually)
The external call target is a trusted, known contract that will not revert
Division denominator is guaranteed non-zero by prior checks or invariants
Balance checks use >= instead of ==
The function handles individual failures gracefully (try/catch, continue on failure)
Remediation
Replace push-payment with pull-payment: let recipients withdraw individually
Use >= instead of == for balance checks to tolerate force-sent ETH
Validate all denominators before division: require(totalShares > 0)
In loops, handle individual call failures without reverting the whole transaction
Use try/catch for external calls where failure should not be fatal
// Pull-payment patternmapping(address=>uint256) public pendingWithdrawals;
function claimPayment() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount >0, "nothing to claim");
pendingWithdrawals[msg.sender] =0;
(boolsuccess,) =msg.sender.call{value: amount}("");
require(success);
}