Solidity Withdrawal Security Pattern
The Problem
The Problem
In Solidity, a standard way of sending Ether to an address is by using the send()
or transfer()
functions. However, using these functions to send Ether directly can pose a significant risk. If the receiving contract contains a fallback function, this could be executed, leading to unexpected behaviour, including the potential for reentrancy attacks. This attack could drain Ether from the original contract.
The Solution: Withdrawal Pattern
The Withdrawal Pattern mitigates this risk by changing the way Ether is transferred. Instead of directly pushing Ether to recipient contracts, they can withdraw their funds. This pattern separates the action of initiating payment and the withdrawal process into two distinct stages.
By storing Ether within the contract, the control is shifted from the sender to the receiver, making it the receiver’s responsibility to initiate the withdrawal. This separation restricts the ability of the receiving contract to interact with the sending contract during the transfer process, significantly reducing the risk of a reentrancy attack.
Use Cases
The Withdrawal Pattern is essential when a contract interacts with other, potentially untrusted contracts. A prime example would be an auction contract, where each bidder is a contract, and the highest bidder gets the bid item.
Implementation
Now, let’s consider a simple implementation of the Withdrawal Pattern:
pragma solidity ^0.8.0;
contract WithdrawalContract {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint amount = balances[msg.sender];
require(amount > 0, "No funds to withdraw");
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed.");
}
}
Here, a user can deposit Ether into the contract using the deposit
function. The balances
mapping keeps track of the balance of each address. Instead of immediately transferring funds, deposited Ether is stored in the contract.
The withdraw
function enables a user to withdraw their balance. Notice the use of call{value: amount}("")
. This pattern is recommended since Solidity 0.6.0 because it avoids the potential pitfalls of send
and transfer
and forces you to deal with possible failures.
This design ensures that the user has to request a withdrawal to get their funds explicitly. Thus, the user contract can’t call back into the contract, mitigating the reentrancy attack.
Advanced Usage: Withdrawal Pattern with Checks-Effects-Interactions
While the basic Withdrawal Pattern already provides a robust defence against reentrancy attacks, it can be enhanced further by integrating it with another best practice: the Checks-Effects-Interactions Pattern. This pattern dictates that you should 1) check conditions, 2) change state, and 3) interact with other contracts in this order.
Implementing the Withdrawal Pattern with the Checks-Effects-Interactions Pattern would look something like this:
pragma solidity ^0.8.0;
contract SecureWithdrawalContract {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint amount = balances[msg.sender];
require(amount > 0, "No funds to withdraw");
// Following the Checks-Effects-Interactions Pattern
// Check
require(address(this).balance >= amount, "Insufficient contract balance");
// Effects
balances[msg.sender] = 0;
// Interactions
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed.");
}
}
We ensure the contract has enough balance between meeting the withdrawal request by introducing an additional check before the withdrawal. This safeguard prevents the contract from entering an inconsistent state.
To conclude, the Withdrawal Pattern is a powerful tool in the Solidity developer’s toolkit. It offers substantial protection against the most notorious smart contract security risk — the reentrancy attack. The judicious use of the Withdrawal Pattern and other security measures like the Checks-Effects-Interactions Pattern can create robustly secure smart contracts. However, it’s important to remember that smart contract security is multi-faceted and requires a holistic approach.
Stay tuned, and happy coding!
Visit my Blog for more articles, news, and software engineering stuff!
Follow me on Medium, LinkedIn, and Twitter.
Check out my most recent book — Application Security: A Quick Reference to the Building Blocks of Secure Software.
All the best,
Luis Soares
CTO | Head of Engineering | Blockchain Engineer | Web3 | Cyber Security | Solidity | Smart Contracts
#blockchain #solidity #ethereum #smartcontracts #network #datastructures #communication #protocol #consensus #protocols #data #smartcontracts #web3 #security #privacy #confidentiality #cryptography #softwareengineering #softwaredevelopment #coding #software