Pull over Push
This pattern is used when we want to withdraw funds from a contract. Let us assume a lottery contract with multiple users. If the lottery is over and the winners are determined, each winner receives their share of the pot.
The Intuitive but Wrong Way – Push
The intuitive and most convenient way to handle the closing of the lottery would be to automatically send the wins to each winner in the same function call where the winner is determined, and the lottery is closed. While this sounds convenient but can cause some serious problems.
Let’s have a look at the code below. It is a very simple contract where any user can trigger the withdraw function that sends the funds to all users in lines 16 – 20.
pragma solidity 0.8.20;
contract push{
mapping (address => uint256) public balances;
address[] public addresses;
uint256 public contractBalance;
function payIn() public payable{
balances[msg.sender] += msg.value;
addresses.push(msg.sender);
}
function withdraw() public{
for(uint256 i = 0; i < addresses.length; i++){ // iterating over arrays of dynamic size can become very expensive and eventually break he contract
if(balances[addresses[i]] > 0){
uint256 payBalance = balances[addresses[i]];
balances[addresses[i]] = 0;
payable(addresses[i]).transfer(payBalance);
}
}
}
function updateBalance() public{
contractBalance = address(this).balance;
}
}
This pattern is called push pattern because the users get their funds pushed to their addresses without doing anything.
The first flaw of this design is the iteration over an addresses array. This might be okay for small arrays, but if the array becomes larger, the cost increases. In the worst case, the cost for iterating over this array are higher than the block gas limit. This would render the withdraw function useless, and all funds get stuck.
The second design flaw is that the user who invokes the withdraw function pays all the gas for sending to the recipients. While this might be desirable in some cases, in our example, it would lead to a situation where no user would be the one who calls the withdraw function first.
And the third flaw of the push pattern is that if the sending contract is not implemented correctly, it can make the whole payment function fail. As a result, one user could hold all the other users hostage by adding a malicious or erroneous contract to the addresses array. Here, we used the transfer method in order to send the Ether. Transfer() reverts on an error.
If you would use send() or call{}() instead, you would get a false as return value that you can handle easily.
The Better Way – Pull
Instead of pushing the funds to all users as we did above, it is better to let every user withdraw their funds individually. This approach has the advantage that if a single user fails to receive his payment due to an erroneous receiving contract, it has no effect on other users. It also doesn’t impose the transaction fees on a single user, and it prevents us from iterating over potentially large arrays and running out of gas.