Ethernaut (Lvl 1): Exploiting the fallback function to claim owership and reduce a smart contract's balance to 0.

Ethernaut (Lvl 1): Exploiting the fallback function to claim owership and reduce a smart contract's balance to 0.

Ethernaut level 1 requires you to exploit a contract with a poorly constructed fallback function . This would allow you to gain control of the contract and also reduce the balance of the account to zero.

Fallback Function

A fallback function is a special function that is executed when a contract receives Ether (ETH) or a token transfer without specifying a particular function to call.

This function is marked with the fallback keyword and does not accept any arguments. It is automatically invoked when someone sends Ether to the contract address without providing any data or when a contract receives tokens (such as ERC-20 tokens) without a specific function call.

Implementing a simple fallback function is considered a best practice if you intend for your smart contract to generally accept Ether from other contracts and wallets.

This fallback function allows the smart contract to mimic the behavior of a wallet, enabling it to receive Ether seamlessly.

By including a fallback function, you allow others to send Ether to your contract without needing to know specific function names or the contract's ABI, enhancing ease of payment.

It's important to note that without a fallback function or known payable functions, smart contracts can only receive Ether as either a mining bonus or as the backup wallet of another contract that has self-destructed.

However, a common issue arises when developers incorporate crucial logic within the fallback function. Examples of such poor practices include altering contract ownership or transferring funds directly within the fallback function.

Ethernaut Level 1 : Fallback

Below is the Vulnerable Contract specified in the Fallback contract specified in ethernaut level 1.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {

  mapping(address => uint) public contributions;
  address public owner;

  constructor() {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

  modifier onlyOwner {
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }

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

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

  function withdraw() public onlyOwner {
    payable(owner).transfer(address(this).balance);
  }

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

The first option requires sending 1000 Ether (1000000000000000000000 wei) to this smart contract. However, this process could take a long time and is impractical due to restrictions on Sepolia faucet requests. Therefore, let's utilize the fallback option instead.

It's important to note that the fallback function has two requirements:

  1. Your account address must have previously donated Ether to this contract.

  2. Your successful fallback function call must include some Ether value.

Using Remix IDE:

  1. Paste the contract code into the Remix UI to provide it with the corresponding ABI.

  2. Ensure you're using the full import path, as Remix doesn't recognize the short path provided by Ethernaut's dApp

  3. A good way to do this is to get the link from the Openzeppelin github repo.

  4. Retrieve your existing contract instance by loading the contract via the instance address. Check the 'instance' variable in the console for your address (e.g., 0xb9bcfd... is the instance address).

  5. Donate a small amount of Ether to the contract using the contribute function. Ensure you're donating from your player account address. Note that the contribute() function includes the condition: require(msg.value < 0.001 ether);, so ensure your contribution value is less than 0.001 ether. Verify that you're donating from your player wallet address.

  6. Finally, add an arbitrary value into the value field and trigger the (fallback) function.

In the console, check that you now own the contract by typing await contract.owner();.

If using the console (not Remix):

You can trigger the fallback function by sending transactions through the console with the following code, ensuring you leave the "data:" field empty:

contract.sendTransaction({
  from: player,
  value: web3.utils.toWei('amount', 'ether') // Replace 'amount' with the desired Ether value
})

Notes on fallback for developers

  1. Keep it Simple: Ensure that fallback functions are straightforwardly implemented to minimize the risk of introducing vulnerabilities or unexpected behavior.

  2. Event Logging: Use fallback functions to log payment events in the transaction history, providing transparency and accountability for payment-related activities within the contract.

  3. Conditional Checks: Employ fallback functions to validate simple conditions before executing actions, thereby enhancing contract security by enforcing specific requirements.

  4. Exercise Caution with Complexity: Be cautious when incorporating complex logic or crucial functionalities into fallback functions. Avoid using them for tasks such as altering contract ownership, fund transfers, supporting low-level function calls, or any other intricate operations that could potentially introduce security vulnerabilities or unintended consequences.

By adhering to these refined security principles, developers can effectively mitigate risks associated with fallback functions and bolster the overall security of their smart contracts.