Solia saysPull-over-push records what each recipient is owed and lets them withdraw — so one malicious recipient that reverts on receive cannot block payouts to everyone else.
Pushing payouts in a loop (contract sends to N recipients) creates a single point of failure: if any one recipient is a contract that reverts in receive(), the entire batch reverts. Pull-over-push inverts the flow: the contract records each recipient's owed amount, and each recipient calls withdraw() to pull their own funds. A malicious recipient can only block themselves.
The pattern shows up in DeFi (Compound rewards), governance (timelock payouts), and games (per-player rewards). The demo shows a push failing on a malicious recipient while a pull-payout serves the other winners cleanly.
Power-ups you unlock
Push: contract sends in a loop; one bad recipient blocks everyone
Pull: contract credits balances; each recipient withdraws their own
A malicious recipient can only block themselves
Common in DeFi, governance payouts, and games
Slightly more gas per recipient but vastly more robust
The Reentrancy Reaper attacks — common mistakes
Mixing push and pull in the same payout (gets the worst of both)
Forgetting to zero a balance before sending (reentrancy)
Pushing to thousands of recipients in one transaction (out of gas)
Letting the credited balance overflow with no cap
Boss battleShow a push payout failing when one recipient reverts, and the pull-payout variant serving the other recipients cleanly.
Example code
<!doctype html><html><head><meta charset="utf-8"></head>
<body style="background:#06040d;color:#e6e0ff;font-family:monospace;padding:20px"><pre id="o"></pre>
<script>
const recipients = [
{ name:'alice', owed:10 },
{ name:'mallory', owed:10, willRevert:true },
{ name:'bob', owed:10 }
];
// PUSH: contract sends in a loop
function push(rs){
for(const r of rs){
if(r.willRevert) throw new Error('push failed at ' + r.name);
r.received = r.owed;
}
}
let pushErr = null;
try { push(recipients.map(r => ({ ...r }))); } catch(e){ pushErr = e.message; }
// PULL: each recipient withdraws their own
const balances = Object.fromEntries(recipients.map(r => [r.name, r.owed]));
const aliceGot = balances['alice']; delete balances['alice'];
const bobGot = balances['bob']; delete balances['bob'];
// mallory cannot withdraw (reverts on receive) — only blocks herself
document.getElementById('o').textContent = [
'PUSH attempt: ' + pushErr + ' (alice and bob also blocked!)',
'',
'PULL pattern:',
' alice withdrew: ' + aliceGot + ' ✓',
' bob withdrew: ' + bobGot + ' ✓',
' mallory still owed: ' + (balances['mallory'] || 0) + ' (only she is blocked)'
].join('\n');
</script></body></html>