The journey continues. Artemis - Week 5
After 4 weeks of classes, we are already past half of the course. Looking back we can see that we have already absorbed plenty of new knowledge and that the basics are already consolidated.
We started the week by doing a deep-dive on how memory and storage work, answering questions such as How do the slots get filled? How does the pointer work? etc. If you want to get more knowledge regarding these topics, make sure to check out the recommended resources at the end of this post.
But that's not all, we also focused delegatecall
, a powerful, but also dangerous, type of external call that can be leveraged to achieve contract upgradability. Before we look into it, let me share with you an easy-to-implement gas optimization tip that you should definitely take into account when developing smartcontracts.
Tips of the week
Sponsored by Gonçalo.
Slot Packing
Slot Packing is an optimization technique that allows us to save gas by using the SSTORE opcode fewer times, thanks to consolidating multiple variables in the same storage slot (32 bytes).
Since the solidity compiler will automatically combine several variables in the same storage slot, you should try to initialize them in such a way that you use the minimum amount of slots.
msg.sender vs tx.origin
Both tx.origin
and msg.sender
are global variables that refer to the caller of a contract. Nevertheless, there are some key distinctions that need to be made.
Weather tx.origin
refers to the original externally owned account that started the transaction, msg.sender
indicates the immediate account (ether an EOA or another contract account) that invoked that interacted with the contract.
In order to prevent security issues, tx.origin
should not be used for authorization. Instead, you should use msg.sender
.
Delegatecall
A delegatecall
is a type of external call that will execute some code of another smartcontract in the context of the caller. Therefore, despite some external code will run, the upgraded state variables will be the ones from the caller.
Because of the described properties, when working with contracts that use delegatecall
, rather than looking at the variable names, we should focus on the slot positions that the external contract will modify in the caller's state.
To help you better understand how delegatecall
works, and to also showcase some of its dangers, let's look into Ethernaut's Level 16 challenge.
Note: Ethernaut is a set of security-focused challenges developed by OpenZeppelin, that help aspiring web3 developers understand all the intricacies of the EVM.
Ethernaut Level 16
In this challenge, our goal is to take over the ownership of the Persistence contract. To do so, we will exploit some vulnerabilities due to a poor understanding of the mechanics behind delegatecall
and storage updates.
As you can see, when using delegatecall
to execute the function setTime()
from LibraryContract
, we will be actually modifying slot0
of the Preservation
contract (instead of the variable storedTime
which points to slot4
).
By properly encoding the attacker contract address into the delegatecall
inputs, we can change slot0
so that timeZone1Library
points to the attacker contract. Once we have managed to do so, we can execute any code we want to take ownership of the Preservation
contract.
Now that we know what the attack will look like, we just need to implement the attacker contract.
Let me remark that to take ownership of the target contract, we need to properly initialize the slot pointers of the Attacker
contract (unlike LibraryContract
did). Then, it is just a matter of modifying the value of owner
(slot3
).
You can find the full implementation of this challenge in my Github repo.
Note: At the time of writing, I am still going over the Ethernaut challenges, so you may not find all the solutions there.
If I had to emphasize something about this post it would be the importance of a mindful slot storage initialization in your contracts, especially if you are dealing with delegatecall
.
Recommended Resources
I highly encourage you to go over the Ethernaut challenges. You will be able to test your understanding of some basic solidity concepts while forcing yourself to code.
If you want to do a deep dive and better understand how storage works, I recommend checking this article by BetterProgramming. The article is really long, but you don’t need to read it all at once or completely.
Although in this post we didn’t go over contract upgradability, we introduced delegatecall
, which is the opcode that enables contract upgradability. If you want to get a better understanding of how do upgradability implementations look like, I encourage you to look into the Universal Upgradable Proxy Standard (also known as just UUPS).