Preventing Denial of Service Attacks in Smart Contracts: Implementing the Withdrawal Pattern
The Pull-Based Withdrawal Pattern: A Defense Against Smart Contract DoS Attacks
There is a hidden danger that can shut down a smart contract.
Picture yourself trying to withdraw money from your bank, but nothing happens every time you press the button. The bank isn’t out of money—it just refuses to process your transaction. Now imagine this happening to thousands of people at the same time.
This is what a denial-of-service (DoS) attack looks like in the blockchain world. Instead of hacking the system, attackers exploit weaknesses in the code to block others from using a smart contract. The contract is still there, but it’s as good as useless for many users.
In simple terms, a DoS attack is like blocking the only exit in a building—people are stuck inside, even though the door is right there.
These attacks can freeze funds, disrupt entire projects, and cause massive losses.
How Can Withdrawals Be Blocked in Smart Contracts?
Today, we will discuss how users can get stuck in smart contracts and the hidden danger of withdrawals.
If you find yourself in a situation where the vending machine that gives out snacks was jammed by someone intentionally and the machine can no longer give anyone a snack, what would you do? That’s exactly what happens with withdrawal functions in smart contracts—they can be jammed or blocked, stopping people from getting their money.
Why are withdrawal functions vulnerable?
Withdrawal functions in smart contracts are particularly vulnerable because they handle money being sent out of the contract.
When a withdrawal happens, the contract needs to send funds to someone’s address. This means:
Now, one broken withdrawal can block everyone. Some contracts process all withdrawals in a single transaction. If one person’s withdrawal fails, the entire transaction reverts, blocking everyone else. Attackers exploit this by deliberately making their withdrawal fail, locking up funds for everyone.
There could also be gas limit problems. If a contract tries to send money to multiple people in a single transaction, the gas cost can become too high.
The Problem with Push-Based Payments
Some smart contracts implement a “push” payment model where the contract directly sends funds to recipients. While this approach seems straightforward, it introduces a critical vulnerability: the contract’s ability to function becomes dependent on the recipient’s ability to receive funds.
Let’s examine a real-world vulnerability found in the Putty Finance protocol and explain how the “withdrawal pattern” can prevent such issues.
In Putty Finance’s case, when users withdrew their funds, the contract first attempted to send a fee to the contract owner before sending the remaining funds to the user:
// send the fee to the admin/DAO if fee is greater than 0%
uint256 feeAmount = 0;
if (fee > 0) {
feeAmount = (order.strike * fee) / 1000;
ERC20(order.baseAsset).safeTransfer(owner(), feeAmount);
}
ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount);
This design created two potential DoS scenarios:
Let’s see what the address zero and the ERC777 are.
The zero address (0x0000000000000000000000000000000000000000) is a special Ethereum address that no one controls. It is commonly used as a placeholder to indicate an uninitialized or non-existent address in smart contracts. Developers use it for various purposes, such as checking if an ownership variable has been set, burning tokens by sending them to an inaccessible location, or preventing accidental transfers. Since no private key exists for the zero address, any funds or tokens sent to it are permanently lost.
ERC777 is an advanced Ethereum token standard that improves upon ERC20 by introducing a built-in hook mechanism that enables smart contracts and external accounts to react when they receive tokens. It maintains backward compatibility with ERC20, meaning it can work with existing ERC20 applications. A key feature is the tokensReceived hook, which allows recipients (especially smart contracts) to execute custom logic upon receiving tokens, enabling automation and security enhancements. ERC777 also supports operator functionality, allowing approved addresses to send tokens on behalf of others, improving usability for wallets and DeFi applications.
Token hooks are callback functions that execute automatically when tokens are transferred. The most notable example is the ERC777 token standard, which introduced hooks as an improvement over the basic ERC20 standard.
When an ERC777 token is transferred, it calls the tokensReceived hook on the recipient's address if the recipient is a contract. This happens before the transfer completes, allowing the recipient to react to incoming tokens.
From the code above, if the baseAsset is an ERC777 token and the owner() is a contract, the owner could implement a malicious tokensReceived hook that deliberately reverts:
// Malicious owner contract
contract MaliciousOwner {
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external {
// Deliberately revert when receiving tokens
revert("Denied");
}
}
Since the owner fee transfer happens before the user receives their funds, this malicious reversion blocks ALL user withdrawals, effectively holding user funds hostage.
The Withdrawal Pattern Solution
The “withdrawal pattern” is a best practice in smart contract design that separates the tracking of balances from the actual transfer of funds. It eliminates this vulnerability by separating the accounting from the transfer.
How It Works
Implementation for Putty Finance
The recommended solution completely separates the fee collection from the user’s withdrawal:
mapping(address => uint256) public ownerFees;
function withdraw(Order memory order) public {
// ... existing code ...
// transfer strike to owner if put is expired or call is exercised
if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) {
// Calculate fee but don't transfer it yet
uint256 feeAmount = 0;
if (fee > 0) {
feeAmount = (order.strike * fee) / 1000;
ownerFees[order.baseAsset] += feeAmount;
}
// Transfer user's portion immediately
ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount);
return;
}
// ... rest of function ...
}
// Separate function for owner to collect fees
function withdrawFee(address baseAsset) public onlyOwner {
uint256 _feeAmount = ownerFees[baseAsset];
ownerFees[baseAsset] = 0;
ERC20(baseAsset).safeTransfer(owner(), _feeAmount);
}
With this design, users can still successfully withdraw their funds even if the owner’s fee withdrawal fails for any reason.
Benefits of the Withdrawal Pattern
Implementing the Withdrawal Pattern in Your Contracts
To implement the withdrawal pattern in your own smart contracts:
The withdrawal pattern is a fundamental security best practice for smart contract developers. By separating balance tracking from fund transfers, it prevents numerous DoS attack vectors and ensures that users maintain control over their assets.
As demonstrated in the Putty Finance example, even well-intentioned contract designs can contain vulnerabilities when they directly transfer funds to recipients during multi-step operations. By adopting the withdrawal pattern, you can build more resilient protocols that inspire user confidence and protect against potential DoS attacks.
Remember: design patterns matter in blockchain security. The withdrawal pattern isn’t just a coding preference—it’s an essential security measure for any contract that handles value transfers.
Always stay safe out there. It does not matter if you are a developer or a user. You should always ask yourself this question: Is the withdrawal pattern followed to prevent denial of service?
I hope you enjoyed this article. I will see you in the next one.