Recovery Challenge - Ethernaut Level 16
Challenge Description
Challenge Goal
Recover the 0.001 ETH that was sent to a lost SimpleToken contract. This requires calculating the contract's address using the Recovery contract's address and nonce, then calling the destroy function to recover the funds.
The Contracts
Here are the smart contracts involved in this challenge:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
string public name;
mapping(address => uint256) public balances;
// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value * 10;
}
// allow transfers of tokens
function transfer(address _to, uint256 _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender] - _amount;
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}The Challenge
In this challenge, someone has deployed a Recovery contract and used it to create a SimpleToken contract. They then sent 0.001 ETH to the SimpleToken contract, but they've "lost" the address. Your goal is to recover that ETH.
The key insight is that contract addresses in Ethereum are deterministically generated. This means that if you know:
- The address of the contract that created the lost contract (the Recovery contract)
- The nonce (transaction count) when the lost contract was created
You can calculate the exact address of the lost contract.
How Contract Addresses Are Generated
Ethereum uses a deterministic algorithm to generate contract addresses. For a contract creating another contract, the formula is:
// Contract address generation formula
keccak256(rlp([creator_address, creator_nonce]))[12:]
// Where:
// - creator_address is the address of the contract that created the new contract
// - creator_nonce is the number of transactions sent by the creator
// - [12:] means we take the last 20 bytes (40 characters) of the hashIn our case:
- Creator Address: The Recovery contract address
- Nonce: 1 (since the SimpleToken was the first contract created by the Recovery contract)
The Solution
Here's the script to recover the lost ETH:
const hre = require("hardhat");
require("dotenv").config();
async function main() {
// Get the Recovery contract address from .env
const recoveryAddress = process.env.RECOVERY_ADDRESS;
if (!recoveryAddress) {
throw new Error("Please set RECOVERY_ADDRESS in your .env file");
}
console.log("Recovery contract address:", recoveryAddress);
// Get the signer
const [signer] = await hre.ethers.getSigners();
console.log("Signer address:", signer.address);
// Calculate the lost SimpleToken address
// Contract addresses are deterministically calculated from the creator's address and nonce
// For a contract creating another contract, the formula is:
// keccak256(rlp([creator_address, creator_nonce]))[12:]
// Since the SimpleToken was the first contract created by the Recovery contract, the nonce is 1
const lostTokenAddress = hre.ethers.getCreateAddress({
from: recoveryAddress,
nonce: 1
});
console.log("Calculated lost SimpleToken address:", lostTokenAddress);
// Check the balance of the lost contract
const balance = await hre.ethers.provider.getBalance(lostTokenAddress);
console.log("Lost contract balance:", hre.ethers.formatEther(balance), "ETH");
// Create an instance of the SimpleToken contract at the calculated address
const SimpleToken = await hre.ethers.getContractFactory("SimpleToken");
const simpleToken = await SimpleToken.attach(lostTokenAddress);
// Call the destroy function to recover the ETH
console.log("Calling destroy function to recover ETH...");
const tx = await simpleToken.destroy(signer.address);
await tx.wait();
// Verify the ETH was recovered
const newBalance = await hre.ethers.provider.getBalance(lostTokenAddress);
console.log("Lost contract balance after recovery:", hre.ethers.formatEther(newBalance), "ETH");
console.log("ETH successfully recovered!");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Step-by-Step Recovery Process
Recovery Steps:
- Get Recovery Contract Address: Obtain the address of the Recovery contract that created the lost SimpleToken
- Calculate Lost Contract Address: Use the deterministic address generation formula with the Recovery contract's address and nonce (1)
- Verify Contract Exists: Check that there's actually a contract at the calculated address
- Check Balance: Verify that the contract has the expected ETH balance
- Call Destroy Function: Use the
destroyfunction to send the ETH to your address - Verify Recovery: Confirm that the ETH has been successfully transferred
Key Concepts
Deterministic Address Generation
Ethereum's deterministic address generation ensures that:
- Contract addresses are predictable if you know the creator and nonce
- No two contracts can have the same address (unless created by the same address with the same nonce)
- Addresses are globally unique across the entire Ethereum network
Selfdestruct Function
The selfdestruct function:
- Permanently removes the contract from the blockchain
- Sends all remaining ETH to the specified address
- Cannot be undone once executed
- Is being deprecated in newer Ethereum versions
Security Implications
- Address Privacy: Don't rely on "losing" contract addresses as a security measure - they can always be calculated
- Deterministic Nature: Understand that contract addresses are predictable and not random
- Selfdestruct Risks: Be careful with contracts that have selfdestruct functions, as they can be used to recover funds
- Nonce Tracking: Keep track of contract creation nonces for security auditing
Real-World Applications
This concept is used in various real-world scenarios:
- Contract Factories: Creating multiple contracts with predictable addresses
- Proxy Contracts: Calculating implementation contract addresses
- Contract Recovery: Recovering funds from lost or forgotten contracts
- Security Auditing: Verifying contract addresses in multi-contract systems
Conclusion
The Recovery challenge teaches us about Ethereum's deterministic address generation and how it can be used to recover "lost" contracts. The key takeaway is that contract addresses are not random - they're calculated using a deterministic formula based on the creator's address and nonce.
This challenge demonstrates why you should never rely on address privacy as a security measure and why understanding how Ethereum generates addresses is crucial for both development and security auditing.