Ethernaut walkthrough [#1]
As someone rather new to the field of smart contracts programming, I thought it might be of help to other beginners to share some of my experiences from different learning resources and how they helped me improve. Beginning with one of the coolest interactive introductions to smart contracts in Solidity — the Ethernaut, built by none other, but the awesome OpenZeppelin team. If you haven’t heard of them, be sure to go and check their work and also pay a visit to the nice Learn section in their documentation. I must point out that this series of posts is aimed towards people with some ground-level knowledge of Solidity and the web3 library. If that’s not the case, I think it would be far more productive to read up on those two topics before diving further. Only by knowing the basics will the challenges make sense.
The solutions can be found here: https://github.com/aleksandar-had/ethernaut-walkthrough .
However, I suggest going through the walkthrough posts first as I tried writing it in an interactive manner — not presenting the solution from the very start, but rather giving the necessary hints to solve the challenges. Of course, if you decide at any point to look at the code straight away and/or you generally learn better by looking at solutions, do it! Then go back and write your version of it!
Now, for the Ethernaut. The OpenZeppelin team has done a great job of introducing it in Level 0, so I will skip that part and jump straight to Level 1.
Level 1 — Fallback
As the name suggests, one should look into the fallback() function of the contract. This is the code that gets executed whenever any value (≥ 1 wei) is sent to the contract’s address outside of its application binary interface (ABI) within a transaction. The key behind claiming ownership and subsequently being able to drain the balance is passing the following require statement in the fallback() function:
require(msg.value > 0 && contributions[msg.sender] > 0);
Find out how to increment your contributions and then send some value to the contract. I suggest that you look into ways of passing a value while calling a contract function and also simply sending value to the contract’s address. Ethernaut’s help can lead you to the correct function maybe.
Level 2 — Fallout
This level introduces a very interesting topic that confused me when I was learning Solidity. How to initialize contracts. In order to do anything with a smart contract, one has to deploy it somewhere (main- or testnet), and to do that, the contract has to be initialized somehow.
Before Solidity version 0.4.22 a contract would be initialized by a function with the same name as the contract itself. For example, a contract named Fallout would have an initialization function:
function Fallout(params...) public
whereas 0.4.22 introduced the use of the constructor keyword and deprecated the old way of initializing. So, don’t get confused like me if you stumble across old Stackoverflow answers with the old syntax for initializing contracts.
In conclusion, this is what this challenge tries to build on. A deprecated fishy 1ooking constructor name. But you shouldn’t be able to call a constructor after the contract has been deployed, right? Unless the constructor isn’t really a constructor…
Level 3 — Coin Flip
Bear with me on this challenge as it introduces several topics (use of Remix IDE, contract deployment, contract storage) and the explanation is rather long.
Coin Flip is a very good intro to the lesson that true randomness in Solidity is hard to achieve. This is due to the fact that if your randomness relies on publicly available values such as the block number, anyone can copy that “randomness” and use it to their advantage. Although Ethernaut doesn’t suggest using an exploit contract yet, I think it’s far more elegant and logical to use one rather solve it via the console. A good abstract analogy for this challenge is the following situation:
Bob comes to you and has a ball in each hand — one is black, the other is white. With his hands behind his back, Bob asks you to guess where the white ball is. If you rely on chance alone, it’s a 50/50 chance, but what if you call Alice, ask her to stand behind Bob, and point to you where the white ball is each time. You will have a 100% success rate.
Substitute Bob with the CoinFlip contract and Alice with the exploit contract that you will write and you have the solution to the challenge. You have to write a contract that copies the “randomness” of CoinFlip and also calls the flip() function. To do so, I suggest using the Remix IDE as it’s designed exactly for Solidity smart contracts development and deployment. Once you’re familiar with Remix’s interface, create a new contract and think of what you’d need. Surely, you would want to have the address of the CoinFlip contract, so maybe include it in the constructor. Other than that, you will need a function that copies CoinFlip’s “randomness” and calls the flip() function with the correct argument value. There are two possible approaches:
- Include CoinFlip’s ABI in your exploit contract by either copying the CoinFlip contract to a Solidity file in Remix and importing it in your exploit contract’s file or including an abstract contract version of CoinFlip with the flip() function signature in the exploit contract’s file. Both approaches are a way for the Solidity compiler to know that behind the CoinFlip address there’s a contract that has a function with the following signature:
- Alternatively, you can provide no ABI information at all and directly send a call to CoinFlip’s address with the bytecode of the function signature and the parameter value.
If this sounds a little confusing, take a look at this great post by Bernard Peh explaining in detail how both approaches actually work with examples. Keep in mind that Solidity docs suggest that developers should change
One last topic that could be introduced is contract storage. Contracts often have state variables that hold some information in them. In CoinFlip’s case, those variables are
consecutiveWins, lastHash, and
FACTOR. By now, you should’ve noticed that lastHash isn’t public and that doesn’t really bother us since we don’t need it for our exploit contract. But what if FACTOR wasn’t public as well and we didn’t know its initial value? Well, that would simply mean we wouldn’t be able to find its value via the contract’s ABI. But since the contract is on the blockchain and all state changes are public and visible, we could use web3’s getStorageAt function. It’s not important for now, but maybe we’ll need it in the future, so I thought it should be introduced.
Hopefully, I didn’t make the walkthrough of this otherwise simple challenge too exhausting, however, I really think it’s far more important to understand the basics behind the exploits rather than simply copy some code.
By now you should have some idea of how to write the CoinFlip exploit contract so all that’s left is to deploy it. Remember that Ethernaut is hosted on the Rinkeby testnet, so that’s where you should also deploy. Remix provides an intuitive interface for deploying and running via an injected web3 provider such as Metamask, but if you run into trouble, they have the process documented.
Did I miss something or maybe got something wrong? Or you simply enjoyed the post? Please, feel free to share your thoughts and comments, I will appreciate it a lot!
Part [#2] out.