What is the pull payment pattern?

A pull payment system stores balances owed to users and lets them claim funds themselves through a withdrawal function. The contract does not attempt to deliver Ether during the original business operation.

This is especially useful when:

  • many recipients must be paid
  • recipients are contracts with unknown behavior
  • payment should not block the main workflow
  • you want to minimize the impact of failed external calls

A common example is a marketplace. When a sale completes, the seller should receive proceeds, but the marketplace should not depend on the seller’s fallback logic. With pull payments, the marketplace records the amount owed, and the seller withdraws later.

Push vs. pull

ApproachBehaviorMain riskBest fit
Push paymentContract sends Ether immediatelyRecipient revert can break the flowSimple one-off transfers to trusted addresses
Pull paymentContract records credit for later withdrawalUser must claim funds manuallyMarketplaces, auctions, reward systems, refunds

The key idea is that payment delivery becomes a separate transaction initiated by the recipient.


Why pull payments are safer

Pull payments reduce the number of external calls made during sensitive state transitions. That matters because external calls can:

  • revert and undo the entire transaction
  • consume unexpected gas
  • execute arbitrary code in the recipient contract
  • create complex dependency chains

By recording a balance instead of sending Ether immediately, the contract can finish its internal logic first. The withdrawal function can then be designed with narrow scope and explicit failure handling.

This pattern also improves operational robustness. If a recipient is temporarily unavailable, the payment remains claimable. The contract does not need to retry or maintain a queue of failed transfers.


A practical implementation

Below is a minimal pull payment contract that credits users and lets them withdraw later.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract PullPaymentVault {
    mapping(address => uint256) private _credits;

    event PaymentQueued(address indexed payee, uint256 amount);
    event PaymentWithdrawn(address indexed payee, uint256 amount);

    error NoPaymentDue();
    error ZeroAmount();

    function queuePayment(address payee) external payable {
        if (payee == address(0)) revert ZeroAmount();
        if (msg.value == 0) revert ZeroAmount();

        _credits[payee] += msg.value;
        emit PaymentQueued(payee, msg.value);
    }

    function paymentsDue(address payee) external view returns (uint256) {
        return _credits[payee];
    }

    function withdrawPayments() external {
        uint256 amount = _credits[msg.sender];
        if (amount == 0) revert NoPaymentDue();

        _credits[msg.sender] = 0;

        (bool ok, ) = payable(msg.sender).call{value: amount}("");
        require(ok, "ETH_TRANSFER_FAILED");

        emit PaymentWithdrawn(msg.sender, amount);
    }
}

What this example demonstrates

  • queuePayment records funds owed to a recipient.
  • withdrawPayments lets the recipient claim their balance.
  • The balance is set to zero before the external call.
  • Events provide a clear audit trail of queued and withdrawn funds.

This structure is intentionally simple, but it captures the core mechanics used in production systems.


The withdrawal flow in detail

A secure withdrawal function usually follows this sequence:

  1. Read the owed amount from storage.
  2. Ensure the amount is nonzero.
  3. Set the stored balance to zero.
  4. Transfer Ether to the caller.
  5. Emit an event.

The order matters. Clearing the balance before the transfer prevents the same funds from being withdrawn twice if the recipient tries to reenter the function.

Why call is preferred over transfer

Historically, many contracts used transfer because it forwarded a fixed 2300 gas stipend. That assumption is no longer reliable for modern Ethereum execution environments. A recipient may need more gas for logging, proxy logic, or other simple operations.

Using .call{value: amount}("") is more flexible and is now the standard approach. The tradeoff is that you must explicitly handle the return value and follow reentrancy-safe patterns.


Common use cases

Pull payments are a strong fit for workflows where the payer and payee are not the same actor.

1. Marketplaces

When a buyer purchases an item, the marketplace can credit the seller’s balance instead of sending Ether immediately. This prevents a seller contract from blocking the sale settlement.

2. Auctions

Auction contracts often need to refund outbid participants. Rather than pushing refunds during every bid, the contract can credit each displaced bidder and let them withdraw later.

3. Revenue sharing

A protocol that distributes fees among multiple stakeholders can record each stakeholder’s share and allow periodic claims. This avoids looping over many recipients during a single transaction.

4. Refund systems

If a deposit must be returned after a condition is met, pull payments make the refund path deterministic and less fragile.


Best practices for production contracts

Keep accounting and transfer logic separate

The contract that calculates who is owed money should not also manage complex recipient behavior. Separate concerns make audits easier and reduce the chance of hidden dependencies.

Use explicit withdrawal functions

A dedicated withdrawPayments() function is easier to test and monitor than automatic transfers scattered across multiple code paths.

Emit events for both crediting and withdrawal

Events are not the payment mechanism, but they are essential for off-chain indexing, support tooling, and operational visibility.

Prefer pull payments for untrusted recipients

If the recipient can be a contract, assume it may revert or attempt reentrancy. Pull payments reduce the blast radius of those behaviors.

Consider batching only at the user layer

If you need to withdraw many credits, let off-chain software coordinate multiple user withdrawals rather than building a large on-chain loop. On-chain loops can become gas-heavy and brittle.


Security considerations

Pull payments are safer than direct transfers, but they are not automatically secure. A few details still matter.

Reentrancy

Even though the balance is cleared before the transfer, the withdrawal function still makes an external call. That means it should remain minimal and avoid additional state changes after the transfer.

A good rule is to update state first, interact second, and keep the interaction isolated.

Denial of service by recipient behavior

A malicious recipient can still revert in its fallback function and prevent its own withdrawal. That is usually acceptable because the failure only affects the recipient’s claim, not the entire contract.

Forced Ether

A contract can receive Ether via selfdestruct or other mechanisms outside the normal payment flow. Do not assume that address(this).balance equals the sum of all credits. Track liabilities separately in storage.

Zero-address handling

Never credit the zero address unless that is a deliberate design choice. It is usually a sign of a bug in upstream logic.


Extending the pattern

In real systems, pull payments often need more structure than a single mapping.

Multiple assets

If your protocol handles ERC-20 tokens as well as Ether, maintain separate ledgers for each asset. Do not mix token balances and native currency balances in one variable.

Role-gated crediting

Only authorized components should be able to queue payments. For example, a marketplace settlement contract may be allowed to credit sellers, while random external callers are not.

Partial withdrawals

Some applications need users to withdraw only part of their balance. In that case, accept an amount parameter and decrement the stored credit accordingly.

function withdraw(uint256 amount) external {
    uint256 owed = _credits[msg.sender];
    if (amount == 0 || amount > owed) revert NoPaymentDue();

    _credits[msg.sender] = owed - amount;

    (bool ok, ) = payable(msg.sender).call{value: amount}("");
    require(ok, "ETH_TRANSFER_FAILED");
}

This is useful for large balances or user interfaces that want to manage cash flow gradually.


Design checklist

Before shipping a pull payment system, verify the following:

  • credits are updated before any external call
  • withdrawals are isolated in a single function
  • failed withdrawals do not affect other users
  • storage tracks liabilities independently of contract balance
  • events are emitted for crediting and withdrawal
  • access control prevents unauthorized credit creation
  • tests cover recipient reverts and contract recipients
ConcernRecommended approach
External call riskUse pull-based withdrawal
Recipient revertFail only the recipient’s withdrawal
ReentrancyClear balance before transfer
Accounting accuracyStore liabilities in dedicated mappings
ObservabilityEmit queue and withdrawal events

Testing strategies

A pull payment contract should be tested against both normal and adversarial recipients.

Test cases to include

  • withdrawal by an externally owned account
  • withdrawal by a contract with a simple fallback
  • withdrawal by a contract that reverts
  • repeated withdrawal attempts after balance is cleared
  • zero-balance withdrawal
  • unauthorized payment crediting

A useful adversarial test is a recipient contract that tries to reenter during withdrawal. Even if the attack fails, the test confirms that state is updated safely before the external call.


When not to use pull payments

Pull payments are not always the right answer.

Avoid them when:

  • the recipient is trusted and immediate settlement is required
  • the payment is tiny and operational simplicity matters more than robustness
  • the user experience cannot tolerate a separate withdrawal step

For example, a simple donation contract may be fine with direct transfers to a single trusted treasury. The pull pattern is most valuable when recipients are numerous, unknown, or potentially hostile.


Conclusion

Pull payments are a practical Solidity design pattern for safer Ether transfers. By recording credits and letting recipients withdraw later, you reduce the risk that a single recipient can disrupt your contract’s core logic. The pattern also improves compatibility with contract recipients, makes failures more isolated, and simplifies reasoning about state transitions.

For marketplaces, auctions, revenue sharing, and refund flows, pull payments are often the most robust default. When combined with careful accounting, minimal withdrawal logic, and clear events, they provide a strong foundation for production-grade value transfer.

Learn more with useful resources