Code and more code. Artemis - Week 4
After getting started with solidity and learning the basics, the next step is to get as familiar as possible with the language. The best way to do so is clear: code as much as possible.
This week we did both active coding tasks, where we had to solve several challenges, and hands-on sessions where we would see the teachers code.
By following this approach, we covered ERC20 and ERC721 tokens (implementing the code ourselves, but also using the OpenZeppelin and Solmate libraries), we created a wETH token contract, a simple staking contract, the bare bones of an AMM, and even a basic voting system based on NFTs.
Since there is a lot of ground to cover, a simple post wouldn’t be enough to go through these implementations. Instead, I recommend checking the ERC20 and ERC721 contracts from OpenZeppelin, since it is an easy way to familiarize yourself with these token standards.
Note: Solmate is also great, but since they follow a minimalist design to optimize gas costs, it is less intuitive for someone new to solidity.
Because of that, this week we won’t be covering any exercises. Instead, we will do a deep dive on a typical security bug: reentrancy attacks.
Last week when going through the differences between send, transfer, and call functions, we saw that it is best to use call()
in combination with Reentrancy Guard or the checks-effects-interactions pattern to protect your functions.
But, what is a reentrancy attack? What are these protection methods? Let’s dive into it.
The Reentrancy Attack
As a smart contract programmer, one of the first things that you should learn is that external calls to untrusted contracts can introduce several unexpected risks and errors. External calls may execute malicious code in your contracts (and also in any other contract that depends on yours), taking over the control flow, and modifying the data that your functions were expecting. As such, every external call should be treated as a potential security risk.
A classical exploit pattern that leverages external calls to drain a smart contract is the reentrancy attack. As described in the diagram below, this type of attack leverages a customized fallback()
function to repeatedly call withdraw()
before the user balance is updated. By doing so, the exploiter is able to effectively transfer all the deposited funds into the vulnerable contract to its own.
Note: If you perform an external static call, you will not have to worry about reentrancy attacks, since won’t be able to perform any status changes.
Check-Effects-Interaction Pattern
A best-practice programming pattern that prevents unexpected executions of a contract. This pattern ensures that the contract author controls all the state variables by following some design principles. The key idea behind the check-effects interaction pattern is that a contract should always check and modify the state variables before performing any external calls.
Let’s break down the design principles into 3 different steps.
Checks: Validate whether the inputs are acceptable or not. Invalid inputs can return
false
and be manually handled, or fail hard and ensure that the transaction reverts. Note that if the transaction reverts, the world state won’t suffer any changes.Effects: Update the contract to a new valid state, assuming that the subsequent interactions will succeed. This step protects the contract from re-entrance and race conditions.
Interactions: Lastly, the contract should invoke any external functions or perform the low-level function
call()
. On a general basis, you should generally limit interactions to "untrusted" contracts. You must always check the result of those interactions.If the called contract "hard fails" with
require()
orrevert()
then the whole transaction will revert. But, if the external callreturns(bool success)
then you need to check for thefalse
condition and manually handle it.
To better understand the described principles, let’s look into some examples.
Example 1: Single-function vulnerability
Example 2: Multi-function vulnerability
Despite you may not be performing an external call directly in one of your functions, you may be doing so in an indirect manner by calling an internal function which, in its turn, performs the external call. Whenever you are facing this scenario, you should treat that internal function (the one which performs the external call) as an untrusted function. Therefore, you must stick to the check-effects-interaction pattern in both functions.
Reentrancy Guard
Another common practice is to use the Mutex pattern, also known as Reentrancy Guard. This method leverages the usage of modifiers (in combination with some checks) to lock the contract and ensure that a given function cannot be reentered until finishes its execution and the contract is unlocked.
Recommended Resources
In case you want to use OpenZeppelin’s implementation of the Reentrancy Guard in your contracts, you can check their GitHub repo.
You may have noticed the small warnings that appear vscode scripts whenever there is an external call. If you want to have them too ―despite being simple it can help you avoid mistakes―, install the Solidity Visual Developer extension that Consensys developed. It has plenty of interesting features.