Ethernaut Lvl 7 Force Walkthrough — How to selfdestruct and create an Ether blackhole
This is a in-depth series around Zeppelin team’s smart contract security puzzles. We learn key Solidity concepts to solve the puzzles 100% on your own.
This levels requires you to send ethers to an empty contract.
What is selfdestruct
selfdestruct is big red button that lets you abandon your smart contract and move all remaining Ethers to another address
selfdestruct(address)
is a low-level opcode call, an alias for the former, not-so-greatly-named, suicide()
function. Unlike other transactions, selfdestruct()
consumes negative gas — so think of it as a garbage collection incentive to clean up void contracts.
When to selfdestruct
Contract owners typically include a selfdestruct
option to be called in the following scenarios:
- To deprecate buggy contracts: When there is a bug or undesired aspect of a smart contract, the creators can then destroy the contract and forward remaining Ethers to a backup address.
- To clean up used contracts that become obsolete. This is seen as a best practice to free up storage on Ethereum blockchain.
A example of how selfdestruct
is often implemented:
function close() public onlyOwner {
//recommended: emit an event as well
selfdestruct(owner);
}
Fun fact: selfdestruct
is currently 1 of 3 methods for your contract to receive ether
Method 1 — via payable functions: Earlier, we discussed that the fallback function is to intentionally allow your contract to receive Ether from other contracts and external wallets. But if no such payable
function exists, your contract still has 2 more indirect ways of receiving funds:
Method 2 — receiving mining reward: contract addresses can be designated as the recipients of mining block rewards.
Method 3 — from a destroyed contract: As discussed, selfdestruct
lets you designate a backup address to receive the remaining ethers from the contract you are destroying.
Caution: be careful about forwarding the selfdestructed ethers to any smart contract.
In this level, we forward Ethers to an empty contract with no withdrawal or transfer capabilities. This means we’re effectively dumping test ethers into a blackhole — never to be used again. Never do this on main-net, because it would tie up the universally-limited number of Ethers (and your own money) forever!
Detailed Walkthrough
- Notice Force.sol is an empty contract incapable of receiving money through a direct transfer. This leaves us with method 2 and 3 left. Let’s
selfdestruct
an aribitrary contract and forward the remaining ether to Force.sol! - In Remix IDE or using truffle framework, initiate the following selfdestructing contract:
contract SelfDestructingContract {
}
3. Allow this contract to receive Ether so it can have a balance:
function collect() public payable returns(uint) {
return address(this).balance;
}
At this point, forward your contract some ethers!
4. Allow this contract to self destruct and forward all remaining ethers to Force.sol.
function selfDestroy() public {
address addr = //your Force.sol instance here
selfdestruct(addr);
}
5. Invoke selfDestroy()
. Your Force.sol instance has now become a permanent sink for all Ethers!
Key Security Takeaways
- Never trust your own accounting: Even as an owner of the contract, you do not control your contract’s balance. Never use contract balance as an accounting or auth check.
- Even if you didn’t implement a selfdestruct(), it is still possible through any delegatecall() vulnerabilities.
- If you implement a
selfdestruct
, i) authenticate that themsg.sender = owner
and ii) emit an event for external dependencies on this contract and for future reference.