DEV Community

Erhan Tezcan
Erhan Tezcan

Posted on

Ethernaut: 17. Recovery

Play the level

// 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

My initial solution was to check the internal transactions of the contract creation transaction of my level instance. There, we can very well see the "lost" contract address, and we will call the destroy function there. To call a function with arguments, you need to provide a calldata (see here). The arguments are given in chunks of 32-bytes, but the first 4 bytes of the calldata indicate the function to be called. That is calculated by the first 4 bytes of the function's canonical form. There are several ways to find it:

  • Use a tool online, such as the one I wrote.
  • Write a bit of Solidity code and calculate bytes4(keccak256("destory(address)")), which requires you to hand-write the canonical form.
  • Write a small contract and run it locally (such as Remix IDE with VM) as follows:
contract AAA { 
  // this is the same function from ethernaut
  function destroy(address payable _to) public {
    selfdestruct(_to);
  }

  // we can directly find its selector
  function print() public pure returns (bytes4) {
    return this.destroy.selector;
  }
}
Enter fullscreen mode Exit fullscreen mode

With any of the methods above, we find the function selector to be 0x00f55d9d. We can then call the destroy function as follows:

const functionSelector = '0x00f55d9d';
await web3.eth.sendTransaction({
  from: player,
  to: '0x559905e90cF45D7495e63dA1baEFB54d63B1436A', // the lost & found address
  data: web3.utils.encodePacked(functionSelector, web3.utils.padLeft(player, 64))
})
Enter fullscreen mode Exit fullscreen mode

Original Solution

Upon sending my solution to Ethernaut, I have learned the actual solution in the message afterwards! Turns out that contract addresses are deterministic and are calculated by keccack256(RLP_encode(address, nonce)). The nonce for a contract is the number of contracts it has created. All nonce's are 0 for contracts, but they become 1 once they are created (their own creation makes the nonce 1).

Read about RLP encoding in the Ethereum docs here. We want the RLP encoding of a 20 byte address and a nonce value of 1, which corresponds to the list such as [<20 byte string>, <1 byte integer>].

For the string:

if a string is 0-55 bytes long, the RLP encoding consists of a single byte with value 0x80 (dec. 128) plus the length of the string followed by the string. The range of the first byte is thus 0x80, 0xb7.

For the list, with the string and the nonce in it:

if the total payload of a list (i.e. the combined length of all its items being RLP encoded) is 0-55 bytes long, the RLP encoding consists of a single byte with value 0xc0 plus the length of the list followed by the concatenation of the RLP encodings of the items. The range of the first byte is thus 0xc0, 0xf7.

This means that we will have:

[
  0xC0
    + 1 (a byte for string length) 
    + 20 (string length itself) 
    + 1 (nonce), 
  0x80
    + 20 (string length),
  <20 byte string>,
  <1 byte nonce>
]
Enter fullscreen mode Exit fullscreen mode

In short: [0xD6, 0x94, <address>, 0x01]. We need to find the keccak256 of the packed version of this array, which we can find via:

web3.utils.soliditySha3(
  '0xd6',
  '0x94',
  // <instance address>,
  '0x01'
)
Enter fullscreen mode Exit fullscreen mode

What is different with soliditySha3 rather than sha3 is that this one will encode-packed the parameters like Solidity would; hashing afterwards. The last 20 bytes of the resulting digest will be the contract address! Calling the destroy function is same as above.

Top comments (0)