Ethernaut#10 - Re-entrancy
Ethernaut #10 - Re-entrancy
The goal of this level is for you to steal all the funds from the contract.
Things that might help:
- Untrusted contracts can execute code where you least expect it.
- Fallback methods
- Throw/revert bubbling
- Sometimes the best way to attack a contract is with another contract.
1 | // SPDX-License-Identifier: MIT |
Contract Breakdown
Let us breakdown the contract to understand what each function and piece of code does. The code consists of a single contract Reentrance compiled with solidity version ^0.6.12.
1 | import 'openzeppelin-contracts-06/math/SafeMath.sol'; |
The contract imports the SafeMath library from OpenZeppelin for safe arithmetic operations.
Variables
1 | using SafeMath for uint256; |
The contract uses SafeMath for uint256 operations and has a public mapping balances that tracks each address’s deposited balance.
Functions
1 | function donate(address _to) public payable { |
The donate function allows anyone to deposit ether into the contract on behalf of another address _to. It uses SafeMath’s add to update the balance.
1 | function balanceOf(address _who) public view returns (uint balance) { |
A simple view function that returns the balance of a given address.
1 | function withdraw(uint _amount) public { |
This is where the vulnerability lives. The withdraw function checks if the caller has enough balance, then sends the ether to msg.sender using a low-level call, and only after that does it update the balance. This ordering is the classic reentrancy bug.
1 | receive() external payable {} |
A simple receive function that allows the contract to accept ether.
Solution
This is the infamous reentrancy attack, the same type of vulnerability that was exploited in the 2016 DAO hack which led to the Ethereum hard fork.
The problem is that the contract sends ether before updating the balance. When the contract sends ether to an attacker contract via
call, the attacker’sreceivefunction gets triggered. Inside thatreceivefunction, the attacker can callwithdrawagain. Since the balance hasn’t been updated yet, the checkbalances[msg.sender] >= _amountstill passes, and the contract sends ether again. This loop continues until the contract is drained.
1 | // SPDX-License-Identifier: MIT |
Steps to pass:
- Check the balance of the Reentrance contract. You can do this with
await getBalance(instance)in the Ethernaut console. - Copy the
HackReentrancecontract to Remix and compile it. Make sure to set the compiler version to 0.6.12. - Deploy it with the Reentrance instance address.
- Call
attack()with a value that evenly divides into the contract’s total balance (e.g., if the contract has 0.001 ether, send 0.001 ether). - The
attackfunction donates our ether to give us a balance, then calls withdraw. When the ether arrives at our contract, ourreceivefunction calls withdraw again, and again, draining the contract completely.
To prevent reentrancy attacks, always follow the Checks-Effects-Interactions pattern: update state variables before making external calls. Alternatively, use a reentrancy guard (mutex) like OpenZeppelin’s
ReentrancyGuard. The order should always be: check conditions, update state, then interact with external contracts.