Most common smart contract bugs of 2020
Based on our audit results — https://github.com/solidified-platform/audits
2020 has been an unprecedented year thus far. Bugs in Ethereum smart contracts are not less of a disaster and don’t seem to be going away anytime soon. The sheer number of vulnerabilities and their severity indicates how important it is to have an audit before deploying smart contracts to the Ethereum main network.
As a blockchain security firm that has done many audits since the inception of Ethereum, we have seen a lot of repetitive but sometimes easily avoidable issues. Since we’re in Q4, we thought we would walk you through the most common bugs and security vulnerabilities we found in our audits during 2020.
We performed over 40 audits this year and reported 35 Critical and high severity vulnerabilities. For the purpose of this article, we listed every issue found in our audits and sorted them by severity, highlighting the most commonly discovered bugs:
We also wanted to be discreet about which smart contracts contained the specific issues, so we intentionally did not include the project names or the exact code where these issues were found. You can always find the complete audit reports on our GitHub repo (solidified-platform).
We hope this information becomes useful to developers in order to build better quality code, users to be aware of the potential risks, and security auditors to compare notes.
It came as no surprise that one of the most infamous bugs in Ethereum is still a common issue to look for while writing a smart contract. The DAO hack caused by the reentrancy bug happened more than four years ago. Since then, many smart contracts fell victim to this issue including the recent ERC777 Uniswap exchange. It is also worth noting that the commonly recommended fix (
send methods) for this issue earlier is no longer valid for all use cases after Ethereum’s Istanbul hard fork with updated opcode pricing.
Reentrancy bugs were present in a few audits we considered for this review and all of them were classified as critical or major bugs since it involved loss of funds in one way or another. Reentrancy bug occurs when an attacker can call a function in the smart contract multiple times before the first execution of the function is completed. Let’s run through an example from one of our audits:
Here in this example, the
deposits mapping of the caller is updated only after the external call. This allows the caller to re-enter the function
safeTransferETH allowing them to withdraw the balance multiple times. Moreover, the
safe in the function name
safeTransferETH may mislead the developers to assume it is “SAFE” to call this function without any precautions.
The best way to avoid the reentrancy bug is to follow the Checks-Effects-Interaction pattern. This pattern ensures that the code does not make an external call until it has completed all the state changes.
Another simple and easy fix to avoid this bug is to use a mutex in functions with possible reentrancy. This may not be the most efficient solution always, but it protects the function from being called again before the lock is released. Using a mutex can sometimes cause other bugs like DoS when used in more complex contracts with multiple interactions. A simple mutex can be implemented as follows.
Here in this implementation, the lock will prevent the function from being called again before the first execution finishes.
A mistake as simple as not restricting an important function from unauthorized access can lead to a disaster. We saw a few contracts this year which missed checking if the caller is authorized to perform a specific action.
The most common place where we found these unrestricted functions were in oracle callback implementations. Let’s take a look at an example which is similar to what we came across.
The price update callback in this contract is intended for the oracle service to callback when a price update is requested. Since the function is not restricted to an oracle service, it can be called by anyone to manipulate the price. To avoid any side effects, it is recommended to restrict such functions to a certain number of trusted callers.
A different variant of this issue can happen when a value in the smart contract (like price) is sourced from the crowd. If there is no penalty on updating to the wrong value, it can be greatly misused.
External calls - DoS
External contract interactions have become essential to many smart contracts in Ethereum. With an increase in the number of interactions the chance of overlooking a vulnerability is also at higher risk. We are always cautious about external calls in smart contracts and we verify them diligently in order not to miss an attack vector.
One such common issue is the Denial of Service bug during ETH transfer. Not all recipients are addresses or contracts with minimal fallback functions. Since the
send function forwards exactly 2300 gas to the recipient, a contract with a fallback function will throw an out-of-gas exception. The intention of this upper limit is to prevent reentrancy vulnerabilities, but it requires gas usage to be constant.
Consider the following example from one of our audits:
This function tries to send ETH to an address and will fail to succeed if the recipient consumes more than 2300 gas. It is recommended to use
call instead of
send to avoid possible DoS.
Using the call function comes with its own risks since all the available gas is forwarded. It is recommended to check the return value of the call method. It is also recommended to always follow the Checks-Effects-Interaction pattern to avoid the reentrancy bug.
Logical errors in the code
This category is not something that is solely related to Ethereum smart contracts or even DApps in general. Logical errors and functional specification mismatch are common issues found in all applications and in smart contracts, this can cause loss of funds or make the application vulnerable to other serious attacks. We decided to include this as part of this report since we found a few serious issues in the audits we performed.
Let’s look at a simple error that makes the contract behave completely opposite to what was initially intended in the whitepaper.
The function calculates the fee based on the amount. If the value is greater than the maximum amount set, a fee is charged. This looks simple and straightforward and should not cause any issue. According to this particular project’s whitepaper, a fee will be collected only if the value is less than the maximum value set — which is the exact opposite of what is being done here. These kinds of simple errors are easily overlooked and the intended code for this function will look something like this.
These bugs are unique to each smart contract and there is no one straightforward solution to fix this. Understanding the detailed specification of the project and writing enough test cases that cover the complete specification and code can help verify the functionality and avoid related bugs. Having an extra developer or an auditor to test the code is also recommended. We at Solidified consider spending time on understanding the specification of the project to be an important step in any audit. For more complex projects, we often jump on a call with the client to make sure that we are on the same page about the specification and intent of the code.
The number of overflow and underflow errors was relatively low this year compared to what we have seen before. It was not completely non-existent however, we could still find it in some unexpected places.
The integer overflow occurs when the integer value reaches the maximum (2⁸ for
uint8, 2²⁵⁶ for
uint256 etc) supported by the data type, causing the value to circle back to 0. This is more common for data types with a smaller maximum value like
uint32. But in some use cases with large enough values,
uint256 can overflow too.
The case with underflow is very similar. When the value of a
uint is set to be less than zero, the value underflows and will be set to the maximum possible value of that data type. Consider the following example of a simple token transfer. If the balance reaches the maximum value, it will be set automatically back to zero.
The easiest and simple solution is to use the SafeMath library which provides functions to perform arithmetic operations with built-in overflow/underflow validations. You can choose to implement the checks yourself but should be extra careful while doing so.
Storing private data
The fact about everything being public on Ethereum is sometimes a bit confusing for new developers. With keywords like
internal used in the Solidity language, the developer can easily mistake them to store private data with restricted visibility.
We have seen many instances where the developer assumes that the contract will store private data and no one in the network can read it. Even worse, some contracts try to implement a monetization model over this. These contracts will allow the user to read the “private” data stored after collecting a certain fee.
The value stored in the secret cannot be accessed from the smart contract — which makes sense for any general-purpose application. However, in Ethereum, storing a value in a private variable does not restrict anyone from reading it from the contract’s state or the transaction which was used to store the same value in the first place.
In short, do not store any sensitive information in Ethereum smart contracts. You can choose to store encrypted or hashed data but it is not recommended.
Gas overflow during iteration — DoS
In some scenarios, loop execution can cost more than the maximum gas allowed on each block and when it happens, the transaction will fail to execute. This can be temporary and will affect only a specific transaction or can be permanent if the iteration count grows overtime. We have come across a few such DoS cases which can occur with or without the presence of an intentional attack.
Let’s assume the function
compute is used in an essential function to dynamically calculate a value. Since the array size can grow to a very large number, the contract will eventually come to a standstill because of the block gas limit.
These are hard to catch during testing since the developers tend to use smaller datasets as inputs and some test networks are often forgiving when it comes to gas limits. It is strongly recommended to avoid loops of unknown size. If looping is necessary for your smart contract, then split the transaction into multiple blocks by keeping track of how far the loop got executed so far. This will help to resume the iteration in subsequent blocks.
Ethereum is a platform which promises to revolutionize a lot of industries, but when compared to other non-Blockchain platforms, it is still in its early stage. A bug-free code is nice to have in other applications, while in a decentralized blockchain like Ethereum where it is designed to be immutable, it is essential.
We covered the most common bugs we came across during our audits this year. Even though these are the frequent ones, you cannot stop looking for other types of bugs. Each contract is unique in its own way and the one non-common bug in your contract can be the most serious one. This makes auditing smart contracts for bugs an essential step before deploying to the main network.
Reminder about testing
Ensuring your smart contract is safe on Ethereum’s main network is not a one-step process. It should be considered from the beginning when the requirements are defined and should be carried out throughout the development phase till the end. The developer should spend enough time to understand the specifications in detail and write enough test cases to cover them in the code. In addition to testing the contract in mock networks, it is encouraged to test them in larger networks like Ropsten to simulate real-world use.
Not all audits are equal
Even with careful planning, development and testing, the resulting contract may still contain some unexpected vulnerabilities. This is more apparent from the number of exploits identified in the deployed smart contracts and the audits we published. Having an experienced team of experts to test and audit the contract will help find such vulnerabilities. There are many firms that can help audit your contracts. But using the traditional methods of auditing may not always yield the best results when it comes to unique problems and that is why at Solidified we use a more decentralized and multi-layered approach to auditing. We employ a multi-layered and decentralized audit technique, involving three or more independent auditors with complementary expertise performing an isolated and unbiased 1:1 audit of smart contracts. Once the independent audits are completed, the auditors come together to have a consensus meeting and combine the results of each audit. This debrief session ensures everyone agrees with the discovered bugs, recommended fixes for each bug and the bug severity. The final report is then published publicly to maintain transparency.
Solidified is the largest smart contract auditor in the US having secured popular projects like Loopring, Kyber, Argent, Gnosis, Nexus Mutual, Polymath and many others, total 450M+ in assets under audit. We are also known for our bug bounty platform where anyone can post their smart contracts to get it reviewed by our community of 250+ security experts.
To learn more about us or request an audit, check out https://www.solidified.io/