Ethernaut Level 1 Fallback [Foundry-Hardhat]

Ethernaut Level 1 Fallback [Foundry-Hardhat]

The Ethernaut-Solutions repository contains the solutions using Foundry and Hardhat.

Objectives

  1. Claim the ownership of the contract

  2. Withdraw all funds from the contract

Go to Ethernaut. Click on level 1. Open the dev console.


Analysis

  • When a function that does not exist is called,

  • When the function selector does not match any function in the contract,

  • When the call is made to the contract with no calldata such as the send, transfer, and call functions,

  • When Ether is sent directly to the contract,

fallback/receive function is called depending on the following conditions.

                       Ether is sent to contract
                                    OR
                           Is msg.data empty?
                                    OR
                           Function not found
                                    /\
                                Yes    No
                                /       \
                         receive()      fallback()
                         exists?         exists?
                          /\                /\
                       Yes  No            No  Yes
                       /     \            |     \
                receive()   fallback()   Error  fallback()
                             exists?
                               /\
                            Yes  No
                            /      \
                    fallback()    Error

To receive Ether, the contract should have receive() function. If receive() function is not present, then the fallback() function must be declared payable to receive Ether.

In the contract, receive() function sets msg.sender as the new owner.

receive() external payable {
  require(msg.value > 0 && contributions[msg.sender] > 0);
  owner = msg.sender;
}

We can trigger receive() function only if

  1. msg.data is empty

  2. msg.value > 0

  3. contribution of msg.sender > 0

Take a look at contribute() function

function contribute() public payable {
  require(msg.value < 0.001 ether);
  contributions[msg.sender] += msg.value;
  if(contributions[msg.sender] > contributions[owner]) {
    owner = msg.sender;
  }
}

We are required to send less than 0.001 ether(1000000 gwei) in msg.value. There’s a require statement that checks if the user’s contribution is more than the owner’s contribution which is 1000 Ether as defined in the constructor. If that’s the case then the user becomes the new owner. We can exploit the contract and become the new owner without spending such a large amount.


Exploit

Open the Dev console on the browser

  • Make the contribution of 1 wei

      await contract.contribute({ value: 1 })
    
  • Now we can trigger the receive() function. Send transaction to the contract keeping msg.data empty.

      await contract.sendTransaction({ from: player, value: 1 })
    

This will trigger receive() function and set your address as the new owner.

  • Check the new owner

      await contract.owner()
    

    This will return your address as you are the new owner of the contract thus fulfilling 1st objective.

  • Call withdraw() function

      await contract.withdraw()
    

    This will withdraw all the funds from the contract, fulfilling the second objective.

  • Check the contract balance

      await getBalance(contract.address)
    

    It should return '0'.

    You have become the owner of the contract and drained it.

    Submit the instance.

Level passed!!!😄

fallback() has a 2300 gas limit when called by transfer() or send(), the most we can do is send ether or log an event. transfer() reverts on error, while send() returns false and will not stop execution. The transfer() uses 2300 gas, which is not adjustable. If the receiving contract needs more gas, it will throw an exception. Always use the check-effects-interaction pattern. Use function modifiers that prevent reentrancy, like Open Zeppelin’s Re-entrancy Guard. If you are using low-level ‘call()’ function, then limit the gas forwarded using gas option.(eg. address(contract.call{gas: 50000}(“”)))


The Ethernaut-Solutions repository contains the solutions using Foundry and Hardhat.

Solution using Foundry:-

Solution using Hardhat:-

More Levels

Did you find this article valuable?

Support Chirag Patil by becoming a sponsor. Any amount is appreciated!