Table of contents
Solidity, the primary language for writing Ethereum smart contracts, has enabled the development of a myriad of decentralized applications. However, like any piece of software, Solidity smart contracts are susceptible to various security vulnerabilities. In this blog, we will discuss some of the most common attacks on Solidity smart contracts and the best practices to mitigate these risks.
Reentrancy Attack
The Attack
A reentrancy attack is a common vulnerability in smart contracts where an attacker is able to repeatedly call a function in the smart contract before the first function call is finished. This can lead to unexpected behavior, as the state variables may be changed during the second function call, affecting the outcome of the first call.
This attack leverages the fact that the contract calls external contracts, which can be hijacked by malicious contracts to regain control and reenter the initial contract. If not handled properly, the malicious contract can drain the target contract's Ether. The infamous DAO attack, which resulted in a loss of $60 million, was a classic example of a reentrancy attack.
Example of a Reentrancy Attack
Let's illustrate this with a simplified bank contract example:
In the withdraw
function, the contract sends Ether to the msg.sender
and then decreases the balance. An attacker can create a contract with a fallback function that calls the withdraw
function again, leading to a loop that drains the contract's Ether:
Mitigating Reentrancy Attacks
The best practice to prevent reentrancy attacks is by employing a principle known as Checks-Effects-Interactions. This principle suggests that you should first validate your inputs (Checks), then change your state variables (Effects), and finally interact with other contracts (Interactions).
If we refactor the withdraw
function in our previous example to follow this pattern, we get:
Now, even if the fallback function of a malicious contract calls the withdraw
function, the balance of the user has already been updated, and the function will fail the require(balances[msg.sender] >= _amount)
check.
The Fix
To protect against reentrancy, it's generally recommended to make use of the Checks-Effects-Interactions pattern, where interactions with other contracts are performed last. Furthermore, Solidity offers a built-in function modifier nonReentrant
from the OpenZeppelin library, which makes a function unable to be called while it is still being executed.
However, the best defense against reentrancy and other attacks is to combine multiple defensive programming practices and to have your contracts thoroughly audited by security professionals.