Halloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanHalloween DealsAmazon USOctober picks are moving - compare before checkoutAmazon US: current deals, useful picks and tech finds.See Picks×
FREEDOM251

Freedom251 › Blog › pull payments

Solidity Pull Payments vs Push Transfers: Designing Safer Web3 Withdrawals

By Freedom251 Team · · 11 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a pull-payment (withdrawal) pattern when a contract must pay untrusted or potentially incompatible recipients. Record what each payee is owed, then let a later withdrawal call deliver the funds. A push payment sends funds during the operation that creates or distributes the payment; that direct call executes recipient code immediately, so a rejection can revert the operation and a reentrant recipient can run before your accounting is safe. Pull payments reduce that coupling, but they still require correct state ordering, failure handling, access control and testing.

Push and pull payments answer different design questions

The distinction is about when the recipient receives control and who starts the transfer.

As an Amazon Associate I earn from qualifying purchases.

  • Push payment: the contract sends value to the recipient as part of the transaction that creates, settles or distributes the payment. The initiating function performs the external call.
  • Pull payment: the contract records an amount owed, and a later withdrawal transaction lets the payee—or, in some designs, another account acting for that payee—claim it.

Solidity’s current “Common Patterns” documentation (the 0.8.38-develop documentation) recommends the withdrawal pattern for funds sent as the result of an effect. Ethereum.org describes the same approach as requiring users to withdraw funds from the contract themselves. The pattern is not a different kind of Ether; it is an accounting and control-flow choice.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why a direct payment call exposes the initiating function

Sending Ether to a contract address is an external call. The target address can contain fallback or receive code, and that code runs before the caller’s transaction continues. The recipient therefore gets execution time inside the operation that is trying to pay it.

That matters when the caller has not yet updated the state that limits a payment. Suppose a contract stores a balance or a pending award, calls the recipient, and only then clears the balance. A malicious recipient can call back into the contract while the old balance is still visible. If the callback reaches a withdrawal or settlement path, the same entitlement may be processed again before the first call finishes. This is the classic checks-effects-interactions failure: an interaction occurs before the effect that should make the interaction one-time.

Reentrancy is broader than Ether transfers. Solidity’s “Security Considerations” documentation warns that calls to other contracts generally can hand control to code that is not trusted. Token transfers, hooks, callbacks and arbitrary function calls can create the same ordering problem.

A recipient can also reject a synchronous push. A reverting fallback, an incompatible contract, or a deliberate refusal makes the low-level call report failure. If the payer uses require(success), the whole initiating transaction reverts. In a loop, one recipient can prevent later recipients from being paid; in a marketplace or auction, delivery failure can block the state transition that should have completed the sale.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

None of this means every push transfer is exploitable. A push can be reasonable when the recipient is controlled, the call is isolated, state is finalized first, and failure behavior is deliberately designed. The risk comes from coupling an important operation to code at an address you do not fully control.

Pull payments separate accounting from delivery

A pull design makes the obligation durable before any recipient code runs:

  1. Calculate the entitlement. Determine the amount owed under the business rules.
  2. Record the debt. Add that amount to a per-payee pending balance, or otherwise finalize the accounting.
  3. Finish the initiating operation. Do not call the payee as part of this step.
  4. Withdraw later. A separate transaction reads the pending amount, clears it, and then performs the external call.
  5. Handle failure explicitly. If the call fails, the withdrawal must not silently lose the recorded amount; the implementation needs a defined revert or restoration path.

The official Solidity example follows checks-effects-interactions by clearing the recorded amount before calling the recipient and checking whether the call succeeded. Clearing first means a reentrant callback sees no remaining balance for that withdrawal. Reverting the transaction on a failed call restores the pre-call state atomically, so a payee can try again.

A minimal withdrawal-pattern example

mapping(address => uint256) private pendingWithdrawals;

function credit(address payee, uint256 amount) internal {
    pendingWithdrawals[payee] += amount;
}

function withdrawPayments() external {
    uint256 amount = pendingWithdrawals[msg.sender];
    pendingWithdrawals[msg.sender] = 0;

    (bool success, ) = payable(msg.sender).call{value: amount}("");
    require(success, "Withdrawal failed");
}

This is a pattern, not a complete production contract. The code that calls credit must ensure that the contract has enough Ether, that credits cannot be fabricated, and that arithmetic and authorization rules match the application. A zero amount may be allowed or rejected according to the interface you want to expose.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The important order is visible: read the amount, set it to zero, then interact. If the call reverts, the entire withdrawal reverts and the zeroing is undone by transaction atomicity. If the call succeeds, a reentrant attempt sees a zero balance for that withdrawal.

Push versus pull: the design trade-offs

Concern Push payment Pull payment
Security and isolation Recipient code runs during the operation that creates or distributes the payment. Safety depends on finalizing state before the call and on handling arbitrary recipient behavior. The original operation records an obligation without invoking the payee. The withdrawal still makes an external call, so its state transition must be safe.
Failure propagation A rejected call can revert the surrounding operation. In a distribution loop, one recipient can block the rest unless failures are isolated and recorded. A payee’s failed withdrawal is normally limited to that withdrawal transaction. Other accounting can complete, and the payee can retry after the cause is fixed.
User experience The sender’s transaction attempts delivery immediately. The recipient does not need a second transaction to claim funds. The payee must submit a later transaction, or arrange for another account to submit it. Someone must provide gas for the withdrawal.
Transaction responsibility The payer or distributing function pays gas for the external call and bears its failure behavior. The withdrawer bears gas and chooses when to claim. A relayer or keeper can submit the call when the interface permits it.
Accounting and implementation The flow can look simpler, but delivery and business-state changes are coupled. Loops, retries and partial failures need careful treatment. Requires pending-payment storage, crediting rules, withdrawal logic and monitoring. More bookkeeping exists, but payment delivery is decoupled from the original action.
Recipient compatibility Contracts that reject or cannot process the call can block the initiating transaction. Incompatible recipients do not block the operation that records their entitlement; their withdrawal remains a separate problem to resolve.

Where push payments go wrong

State is changed after the call

The dangerous shape is “send, then mark paid.” If the recipient reenters before the mark, every check that relies on the old state can pass again. Move all effects that protect the invariant before the interaction. For a one-time payment, that usually means setting the claimable amount to zero before calling the payee.

A recipient rejection becomes a business-logic failure

Consider a function that distributes a prize to several addresses in one transaction. A single recipient whose fallback reverts can make the entire transaction fail if the loop requires each call to succeed. The winner list may be correct, but no distribution completes. A pull ledger records each winner first and lets each winner handle delivery independently.

Ignoring a failed low-level call loses the intended outcome

Using a low-level call without checking its boolean result can make the contract report a successful payment when the recipient received nothing. If you use a call, decide whether failure should revert, preserve the debt for retry, emit an event for operational handling, or follow another explicit policy. “Continue anyway” is not a failure strategy unless the accounting records exactly what remains owed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Relying on the old transfer safety slogan

Older tutorials often describe Solidity’s transfer as intrinsically safe because of a fixed gas stipend. That simplified claim is not a substitute for modern review. Gas costs and recipient behavior can change, and reentrancy is not limited to Ether sends. Current Solidity guidance favors the withdrawal pattern for effect-triggered payments and emphasizes state ordering and broader cross-contract interactions. Do not choose push solely because a snippet uses transfer.

Why pull payments do not remove all security risk

The withdrawal function still calls an address supplied by the payee. Forwarding all available gas lets recipient code execute freely. OpenZeppelin’s Contracts 3.x PullPayment documentation warns about this reentrancy exposure and points to trusting the recipient, following checks-effects-interactions, or using ReentrancyGuard. That guidance is version-specific: the cited API page is for the 3.x line, while other OpenZeppelin documentation explicitly covers 4.x and older versions. Match any library advice to the version actually deployed.

  • Protect the withdrawal state. Clear or otherwise consume the claim before the external call. A mutex such as a reentrancy guard can add defense in depth, but it does not repair incorrect accounting.
  • Preserve failure semantics. If the recipient call fails, make sure the debt is still claimable. Reverting the withdrawal is one common approach; swallowing the error requires a separate, correct re-credit path.
  • Check cross-function paths. Reentrancy may enter a different function that shares the same balance, auction, loan or reward state. Review the invariant across every external entry point.
  • Control who can credit funds. A secure withdrawal routine cannot compensate for an authorization bug that lets anyone increase another account’s pending balance.
  • Account for forced Ether and solvency. The contract’s recorded liabilities and actual balance can diverge. Define what happens if the contract is underfunded, receives unexpected Ether, or has funds reserved for another purpose.
  • Emit useful events. Credit and withdrawal events make pending obligations observable and help operators identify stuck claims without changing the security model.

Escrow-based implementations and third-party withdrawals

OpenZeppelin’s documented PullPayment implementation places funds in an intermediate Escrow contract. The payer records an amount for a payee, and the payee retrieves it through withdrawPayments. Its documentation states that any account can call the withdrawal function for a payee. That option is useful when a protocol cannot assume the payee can submit a transaction—for example, when a contract recipient has no convenient user interface or another account is sponsoring gas.

Allowing a third party to trigger a withdrawal does not transfer ownership of the funds. The implementation must still send to the recorded payee, not to the caller, and must prevent the caller from changing the payee or amount. Treat the documented API and trust assumptions as tied to the library version you select; do not copy an older interface into a newer deployment without checking its current behavior.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choosing between push and pull in a contract design

Prefer pull when recipients are untrusted or numerous

  • The recipient set includes arbitrary user wallets or contracts.
  • One recipient’s failure must not block settlement for everyone else.
  • Payments are prizes, refunds, royalties, salaries, rebates or other liabilities that can wait for a claim.
  • The protocol can expose a clear pending-balance and retry experience.

Push can be acceptable when delivery is deliberately bounded

  • The recipient is a trusted component whose code and upgrade path you control.
  • The payment is a small, isolated side effect and a failed call has a defined, tested outcome.
  • All relevant state is finalized before interaction, and no callback can violate a shared invariant.
  • The function does not rely on a fragile loop in which one recipient can block unrelated recipients.

Even in these cases, document why push is required. “It is shorter” is not a sufficient security or reliability rationale.

Implementation checklist for a withdrawal function

  1. Identify the liability. Write down exactly what creates a credit, who may create it, and whether multiple credits accumulate.
  2. Use safe arithmetic and authorization. Ensure credits cannot overflow or be assigned by an unauthorized caller, and define behavior for zero-value credits.
  3. Make the claim single-use. Read the pending amount and consume it before the external call.
  4. Use an explicit value-transfer result. Check the result of call; do not assume a non-reverting Solidity statement means the recipient was paid.
  5. Decide who may submit the transaction. Restrict withdrawal to the payee, or allow a third party while always paying the recorded payee and preventing caller-controlled redirection.
  6. Bound or reject unexpected conditions. Specify what happens when the contract lacks enough Ether, when the recipient is a contract, and when a withdrawal is attempted twice.
  7. Test adversarial recipients. Include a receiver that reenters, one that reverts, one that consumes substantial gas, and one that accepts payment but performs callbacks into unrelated functions.
  8. Test isolation. Credit several payees, make one withdrawal fail, and verify that other credits remain intact and claimable.
  9. Review upgrades and libraries. Confirm the exact Solidity compiler and payment-library versions, because documentation and APIs differ across releases.

Testing scenarios that expose the real difference

Reentrant receiver

Deploy a receiver whose fallback calls the withdrawal function again. A correctly ordered pull implementation should make the second call observe no remaining credit. Test reentry into other functions too; a guard on one function does not prove that shared state is safe everywhere.

Reverting receiver

Use a receiver that always reverts. In a push distribution, verify whether the entire operation reverts or whether the contract records the failed recipient for later handling. In a pull design, verify that a failed withdrawal leaves the credit available after the transaction ends.

Multiple payees

Credit several accounts, then make one payee’s withdrawal fail. The other accounts should still be able to withdraw. This test demonstrates whether obligations are isolated rather than coupled through a distribution loop.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Third-party caller

If the interface permits anyone to trigger a payee’s withdrawal, call it from an account that is not the payee and confirm that funds still go to the recorded payee. Also verify that the caller cannot substitute a different recipient.

Standards and current guidance

The OWASP Smart Contract Security Verification Standard 0.0.1, dated 2024-10-04, includes a business-logic requirement that the withdrawal process use a pull-based approach to track accounting and allow users to pull payments. This is a stated control in that standard, not a legal requirement or proof that every contract must use pull payments.

The common thread in the Solidity, Ethereum.org and OpenZeppelin guidance is conditional: isolate recipient behavior where practical, finalize accounting before external interaction, and make failure and retry behavior explicit. Selecting pull over push improves isolation, but it does not replace review of invariants, authorization, solvency or reentrancy across the whole contract.

Practical decision

If your contract pays addresses you do not control, start with a pending-payment ledger and a withdrawal function. Treat the later withdrawal as an untrusted external interaction: consume the credit first, check the result, and test hostile recipients. Choose push only when immediate delivery is genuinely required and you can explain—and test—why recipient behavior cannot compromise the surrounding operation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

pull paymentspush paymentsReentrancySmart Contract SecuritySolidityWeb3 Developmentwithdrawal pattern

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.