Why use the withdrawal pattern?
· Jim McDonald · 4 minutes read · 693 words ·The article Building Ethereum payment channels⧉ gave rise to the question of why closing a channel adds funds to a balance rather than simply returning funds to the channel participants. This mechanism is known as the withdrawal pattern, and this article provides an explanation of why it is necessary.
The issue arises when an Ethereum transaction attempts to release funds to an address not under the control of the person sending the transaction. A much-simplified example contract that does this is shown below:
contract BadSplitter {
uint256 funds;
address sender;
address recipient;
// Deposit some funds in to the contract
function deposit(address other) payable public {
funds = msg.value;
sender = msg.sender;
recipient = other;
}
// Split an amount of funds between the sender and the recipient
function split() public {
// Can only be called by the recipient
require(msg.sender == recipient);
// Split the funds
sender.transfer(funds / 2);
recipient.transfer(funds / 2);
}
}
(Please note that this contract is purely for illustrative purposes and is totally unsuitable for any real work.)
The way this contract operates is as follows:
- the sender sends a deposit() transaction, with the address of the recipient
- the recipient sends a split() transaction, which sends half of the deposit to the recipient and half back to the sender
Or graphically:
There are no loops or conditionals so control flow is easy to follow. What could go wrong?
The issue is due to the fact that accounts and contracts are both valid participants in transactions, and that contracts do whatever they have been programmed to do when they receive funds as part of a transaction.
When a contract receives funds it invokes a special function on the contract, called the fallback function. This allows the contract to have control over the control flow of the transaction that invoked it.
A purely malevolent contract is shown below:
contract BadSender {
// Forward funds to the splitter contract
function forward(address other) payable public {
// This is the address of the splitter contract
address splitterContract = 0xAe3aE77F5ab2490C46958D0b05f766871c17cA5e;
BadSplitter splitter = BadSplitter(splitterContract);
splitter.deposit.gas(200000).value(msg.value)(other);
}
// Purely malevolent fallback
function () {
revert();
}
}
The way this contract operates is as follows:
- the sender sends a forward() transaction, with the address of the recipient. This forwards the call to the BadSplitter contract described previously
- when the recipient attempts to call split() in the BadSplitter contract they find that their transaction fails. This is because BadSplitter’s call to sender.transfer() invokes the fallback function in BadSender, which immediately fails and causes the entire transaction to fail
Or graphically:
It is important to understand that reverting the transaction cancels all actions taken by it, so the state of Ethereum is as if the transaction never took place. If this were not the case, the contract could simply be written to send funds to the recipient before the sender and avoid the issue.
As mentioned above this is a purely malevolent contract, in that it denies the recipient access to their funds unconditionally. Given that a smart contract is a program, however, a more advanced BadSender could allow or deny the transaction based on time, balance of another account, an internal flag etc. The malicious possibilities are endless and allow the sender to hold the recipient to ransom.
In the context of payment channels, this has negative implications because it would be possible for a bad sender to revert the transaction whenever the recipient attempts to close the channel, denying the recipient their promised funds. Once the expiry time for the channel has closed the sender could expire the channel and receive all of their deposited funds and permanently depriving the recipient of their promised funds.
The solution to this issue is to use the withdrawal pattern. By keeping track of balances internally within the contract and forcing each user to withdraw their own funds it makes redundant the fact that the other party in the transaction might be a malicious contract. More details about the withdrawal pattern are available in the official solidity documentation⧉ .