Ethernaut level one: a fallback function is a public entry point
Work the Ethernaut levels rather than read the writeups.
ethernaut/fallback.sol is level one and it teaches the thing most Solidity
vulnerability lists describe badly. The contract has a receive function that
assigns ownership to whoever sends it a bare transfer, guarded only by having
previously contributed. The exploit is two transactions: contribute a tiny
amount to satisfy the guard, then send a plain transaction with no data, which
lands in the payable fallback and hands you the contract. Then withdraw.
The general lesson is not “do not put ownership changes in a fallback”, which is true and too specific to use. It is that a fallback is a public entry point running on any call the contract does not otherwise match, including calls nobody wrote a function for. Every state change inside one is reachable by paths the author never enumerated, because the author was thinking about functions.
receive and fallback being separate in modern Solidity helps by making the
bare-transfer case explicit. This is ^0.6.0, which predates that being as
clear as it is now.
Working the levels is genuinely different from reading about them: read, the bug is obvious; sitting in front of the contract without being told which level it is, much less so.