Ethernaut Walkthrough — Level 7: Force

Published on Dec 10, 2021

Level 7 presents us with an empty contract containing nothing more than some ASCII-art. The only hint we get is that some contracts won't accept our money.

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

This level probably let's you search around for ways a contract can receive money. You probably know about payable functions and fallback methods, but the contract doesn't seem to contain any of these.

I tried logging the full contract ABI on Ethernaut, but it seems to be empty so that's where I ran out of ideas pretty quickly.

The hack

After ending up on stackoverflow and a bunch of other sites I found out about the selfdestruct method in Solidity. I knew this could be used to destroy a contract on the blockchain, but I didn't know that any leftover funds in the contract could be pushed to another contract. The destination contract has no choice but to accept the Ether.

From the docs:

The only way to remove code from the blockchain is when a contract at that address performs the selfdestruct operation. The remaining Ether stored at that address is sent to a designated target and then the storage and code is removed from the state.

This brings us to Remix where we'll create a simple contract that contains a method to receive Ether, I've used a payable constructor to do so. Make sure you send it a small amount of Ether when deploying the contract through Metamask.

In the contract, I've defined a function that invokes the selfdestruct method, sending any Ether left in the contract to the address of our Ethernaut instance. The Ethernaut contract wil lhave no choice but to accept the Ether, letting us pass this level.

Here's the contract I've used.

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

import "hardhat/console.sol";

contract Hack {
    constructor () payable {
      // this allows us to send some Ether into the contract 
    }

    function getBalance() public view returns (uint256) {
      return address(this).balance;
    }

     function die () public  {
      // selfdestruct will force any ether in this contract to the address below
      selfdestruct(payable(address(0x3d9a3b4bBE08805775bA5c6D74F129C205965f09)));
    }

}

Security lessons learned

  • never rely on code like address(this).balance == 0 for any contract logic, because we know the balance can be manipulated by sending it Ether via the selfdestruct method
  • instead, use your own mapping to keep track of balances on your contract and for any logic, since these balances are only indirectly related to address(this)balance and can't be manipulated by sending Ether via selfdestruct

Continue from here

Here's my solution for level 8

No comments? But that’s like a Gin & Tonic without the ice?

I’ve removed the comments but you can shoot me a message on Twitter @GoodBytes to keep the conversation going.