Solidity Pull Payments
Pull payments are withdrawals by the people who are owed. The contract records each amount owed, and each person claims it in a separate call.
Amounts owed are recorded, not sent
Distributing records an amount for one address. The ether stays in the contract until that address claims it.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract PullPayments {
address public owner;
mapping(address => uint256) public owed;
constructor() {
owner = msg.sender;
}
receive() external payable {}
function distribute(address to, uint256 amount) public {
require(msg.sender == owner, "not owner");
owed[to] += amount;
}
}Each person claims exactly once
The owed person withdraws the recorded amount. The owed amount is cleared first, so a second claim reverts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract PullPayments {
address public owner;
mapping(address => uint256) public owed;
constructor() {
owner = msg.sender;
}
receive() external payable {}
function distribute(address to, uint256 amount) public {
require(msg.sender == owner, "not owner");
owed[to] += amount;
}
function withdrawPayment() public {
uint256 amount = owed[msg.sender];
require(amount > 0, "nothing owed");
owed[msg.sender] = 0;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "payout failed");
}
}One claim leaves other amounts untouched
Two people are owed different amounts. The first claim pays only its own amount and leaves the second amount in place.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract PullPayments {
address public owner;
mapping(address => uint256) public owed;
constructor() {
owner = msg.sender;
}
receive() external payable {}
function distribute(address to, uint256 amount) public {
require(msg.sender == owner, "not owner");
owed[to] += amount;
}
function withdrawPayment() public {
uint256 amount = owed[msg.sender];
require(amount > 0, "nothing owed");
owed[msg.sender] = 0;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "payout failed");
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract PullPayments {
address public owner;
mapping(address => uint256) public owed;
constructor() {
owner = msg.sender;
}
receive() external payable {}
function distribute(address to, uint256 amount) public {
require(msg.sender == owner, "not owner");
owed[to] += amount;
}
function withdrawPayment() public {
uint256 amount = owed[msg.sender];
require(amount > 0, "nothing owed");
owed[msg.sender] = 0;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "payout failed");
}
}Key points
- Owed: an amount the contract records for one address.
- Claim: a separate withdrawal call by the owed person.
- Exactly once: the owed amount is cleared before the payout, so a second claim reverts.
- Isolation: one person claiming does not change what others are owed.
- Funding: the contract holds the ether until each claim is made.
In this example, the owner records 2 ether for one address and 3 ether for another, with the contract funded by 10 ether. Each address claims in its own call and receives exactly its recorded amount. A second claim by the same address reverts because the owed amount was cleared before the first payout.
Note
a failed claim for one person never blocks the claims of others.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
In pull payments, who starts the payout?