If you are a Solidity developer like me, the Withdrawal from Contracts pattern is probably one of the first Common Patterns you learn. It can protect your contract from failing when sending funds to another address. However, the trade-off is that there is an additional step introduced, which increases complexity and can disturb the automation process.
☝️ If you are not familiar with the Withdrawal from Contracts pattern, read on. Otherwise, you can skip to the Destructible Transfer pattern section.
Case Study - Payroll system
Imagine you are developing an automatic Payroll system which sends payments in Ether. All the hard parts are done - the only thing left to do is to write a Smart Contract that will process and log each payment on the Ethereum blockchain by emitting an event. Here is your first draft:
contract NaivePayroll {
event Paid(address account, uint amount);
function pay(address account) public payable {
require(account.send(msg.value));
emit Paid(account, msg.value);
}
}
Security risk
The contract above is named NaivePayroll
for a reason - it naively assumes that the receiving address is either a wallet, or a contract that won’t do anything weird. Consider the following contract:
contract MaliciousReceiver {
function() public payable {
revert();
}
}
The unnamed function is a fallback function - it gets executed whenever there is an Ether transfer to this contract.
💥 If the MaliciousReceiver
is on the payroll, your automation system will be sending failing transactions to the blockchain.
The solution
You designed the automation system to either skip these accounts or gracefuly handle the failing transactions. However, this is not enough - the intent to pay must be logged on the blockchain, and the funds must leave the automation system’s account.
There are two solutions to this problem:
- Withdrawal from Contracts pattern
- Destructible Transfer pattern
Withdrawal from Contracts pattern
You quickly change your NaivePayroll
using the Withdrawal from Contracts pattern. Here’s your code:
contract BrokePayroll {
mapping(address => uint) pending;
event PayAdded(address account, uint amount);
event PayWithdrawed(address account, uint amount);
function pay(address account) public payable {
pending[account] = pending[account] + msg.value;
emit PayAdded(account, msg.value);
}
function withdrawPay() public {
require(msg.sender.send(pending[msg.sender]));
emit PayWithdrawed(msg.sender, pending[msg.sender]);
pending[msg.sender] = 0;
}
}
Security risk
Again, the name of the contract is not without reason. Consider the following contract:
contract GreedyReceiver {
function getPaid(address _payroll) public {
Payroll payroll = Payroll(_payroll);
payroll.withdrawPay();
}
function() public payable {
if (msg.sender.balance >= msg.value) {
this.getPaid(msg.sender);
}
}
}
💸 If the GreedyReceiver is on the payroll, it can withdraw more funds than intended.
The fallback function of the GreedyReceiver
initiaties a withdrawal again and again, until there are no funds left and you are broke.
☝️ This problem is referred to as the Reentrancy problem
Correct Withdrawal from Contracts pattern usage
The solution to reentrancy is rather simple - all you need to do is to clear the pending withdrawals first, before sending the funds.
contract WithdrawalPatternPayroll {
mapping(address => uint) pending;
event PayAdded(address account, uint amount);
event PayWithdrawed(address account, uint amount);
function pay(address account) public payable {
pending[account] = pending[account] + msg.value;
emit PayAdded(account, msg.value);
}
function withdrawPay() public {
uint value = pending[msg.sender];
pending[msg.sender] = 0;
msg.sender.send(value);
emit PayWithdrawed(msg.sender, value);
}
}
Destructible Transfer pattern
The alternative to using the Withdrawal from Contracts pattern is to use the Destructible Transfer.
The core of this pattern is a small but interesting contract:
contract DestructibleTransfer {
constructor(address payee) public payable {
selfdestruct(payee);
}
}
The DestuctibleTransfer
contract acts as an intermediary contract used for conducting a payment. When constructing, it immediately self-destructs, which causes the remaining funds to be transferred to a given address.
The DestuctibleTransferPayroll
contract looks like this:
contract DestuctibleTransferPayroll {
event Paid(address account, uint amount);
function pay(address account) public payable {
(new DestructibleTransfer).value(msg.value)(account);
emit Paid(account, msg.value);
}
}
The DestuctibleTransferPayroll
contract is not vulnerable to both MaliciousReceiver
and GreedyReceiver
.
Why it works
The transfer initiated from a destructing contract does not execute the reciver’s fallback function.
Therefore, failures or reentrancy in the fallback function code are no match for our Payroll system utilising the Destructible Transfer pattern.
Downsides
The Destructible Transfer is using more gas than the ordinary transfer. In order to reduce gas usage, consider using the Destructible Transfer only when the standard transfer fails.
contract SmartPayroll {
event Paid(address account, uint amount);
function pay(address account) public payable {
if (!account.send(msg.value)) {
(new DestructibleTransfer).value(msg.value)(account);
}
emit Paid(account, msg.value);
}
}
Conclusion
In Smart Contract development, security is very important, especially when dealing with Ether deposits and withdrawals. Every security breach puts a stain on community’s trust in the Ethereum’s applicability for real world businesses.
I believe that estabilishing Solidity patterns and best practices will improve general quality of Smart Contracts on the Ethereum blockchain, and hope that my contribution will help the developers to write secure Smart Contracts with confidence.
Top comments (0)