Ethernaut walkthrough [#2]
This is the second part of the walkthrough on the Ethernaut challenges. Part [#1] covered some basics (Remix IDE usage, ABIs, and other things) including the solutions to the first 3 levels.
As previously mentioned, the solutions could be found here:
however, I suggest trying to solve the challenges first as the posts are structured in a walkthrough fashion (with hints). Of course, if you feel stuck at any moment, look at my interpretations and try again yourself.
Level 4 — Telephone
It’s clear that to gain ownership of the contract, you have to pass the require statement of the changeOwner function. This challenge introduces the notion of calling a contract via another deployed contract. To do this, you can use Remix IDE (if you’re unfamiliar with that topic, take a look at my walkthrough of Level 3 in the first post).
Now, what are these
In the end, you have to deploy a contract that has a function to call the changeOwner function of the challenge contract. I’ve gone for an ABI-less approach since it’s simple enough. If you feel like practicing different ways to call the changeOwner function, go for it.
Level 5 — Token
Now, this might seem like a very solid contract at a first glance (here comes the but) BUT there’s a very important thing to remember. The thing that so much of the modern and widely spread programming languages take away from us and maybe we don’t even bother thinking about it sometimes…
What is the type of variable which you’re defining/declaring?
I’ve noticed that, when it comes to Solidity, people sometimes simply write the uint keyword without really paying attention to it (I’ve caught myself doing it several times, so… guilty as well).
Now that I’ve pointed that out, let’s see if that would be a potential issue. Is there a possibility in the contract that an unsigned integer is forced to the unknown side of negative numbers? Is there a subtraction? Yes, there is. There you have it, your exploit. All it takes is to invoke the transfer() function with a value greater than your starting balance of 20 tokens.
Surely, such a simple operation as subtraction would not pose a security breach and would be handled gracefully by the language itself, right? Well, yes and no. Don’t get me wrong, this was never a “hidden” bug of any sort. It was always more than clear that it’s possible for things to get messy if you have unsigned integers and subtraction which can lead to negative numbers. The bottom line is that it was left for the developers to be aware and cautious of overflow in their contracts. As it should be, if you ask me. This approach was discussed from the very beginning. The result was the creation of the SafeMath library. It became such an essential import to almost every Solidity contract that the Solidity developers ultimately opted for its adoption in the 0.8 release.
For some deeper analysis of the whole overflow problem, check this post by Mikhail Vladimirov.
Level 6 — Delegation
This challenge is an ode to one of the most famous “mishaps” in Ethereum 1.0’s history — the Parity bug. The focus point on this level is the difference between the low-level functions call and delegatecall. The first is familiar from previous challenges as a way to call external contract functions and send ether. The latter is identical to call apart from the fact that the code at the target address is executed in the context of the calling contract, so
msg.value don’t change their values. As always, I’d suggest reading the official documentation on that topic. Additionally, here is a visual representation of the difference between call and delegatecall.
I think that this information is enough for now, anything more might feel overwhelming. If you find it interesting, however, explore further.
So, to call the
pwn() function, you have to encode the function signature and pass it along as data within your transaction to the challenge contract. You can do that either as we’ve previously done — presenting the function signature and arguments as a string, or by passing a JSON object. Examples of both approaches can be found in the web3 docs.
Level 7 — Force
So, you reached another point where you have to send some ether to a contract. You could try and simply send some ether to the contracts’ address. Go ahead and try that out to see if it works. If you don’t remember how that worked exactly, take a look at the help for the
sendTransaction() function (or go back to the solution of Level 1).
My bet is that you received some error in your browser console stating that the execution reverted. Let’s take a look at why that happened. A smart contract on the blockchain has an address, just like you have your own address with which you interact with Ethernaut. The difference is that there’s no “code” deployed on your address so whenever it receives ether, there’s no logic to be invoked. A smart contract, however, acts similarly to a public API — it can be called by external addresses by sending the right payload (that matches the contract’s functions), just like you have to call the right API endpoints, otherwise nothing happens. By default, the contract can’t receive ether through a function call, unless that function is marked as
payable. Whereas, to simply send ether to a contract, without invoking any function at all, the contract must either have the receive or fallback functions defined. You are facing a contract that has neither.
Does that mean there’s no way to send ether to that contract’s address? No, there are several ways to do that.
The first one is to set the contract’s address as the recipient of a miner block award. Every time someone successfully mines a block on Ethereum 1.0’s blockchain, the miner is entitled to a block award. Usually, the miner claims that award but they can always point to some address and set it as the block award’s recipient. This can’t be simulated on the Rinkeby network by us, so let’s look at another possibility.
A second way to send ether to a contract’s address irreversibly is to put that address as the target of another contract’s selfdestruct function. Selfdestruct sends all remaining ether stored in a contract to a designated address.
You should’ve gotten a clue how to solve this by now. Create a contract with some amount of ether in it (you can specify a value when deploying a contract on Remix, just make sure that your constructor is marked as payable) and then have a function that calls selfdestruct pointing to the challenge contract.