Ethernaut Walkthrough — Level 7: Force
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.
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