Smart contracts are hard to get right. Their three main properties, the ability to hold value, transparency, and immutability, are essential for them to work. However, these properties also turn smart contracts into a security risk and a high-interest target for cybercriminals. Even without deliberate attacks, there are plenty of examples of funds getting stuck and companies losing money due to smart contract bugs and vulnerabilities.
Over the last two years, we have audited the smart contracts of more than 40 projects here at Cryptonics. The contracts audited include different types of asset tokenization, insurance policies, decentralized finance platforms, investment funds, and even computer games. We have observed certain trends in the types of vulnerabilities that we usually encounter, and some issues seem more common than others. In this article, we will describe the five most common issues we detect in our daily auditing activities.
1. Integer Arithmetic Errors
A very common occurrence is getting integer arithmetic wrong. Smart contracts generally express numbers as integers due to the lack of floating-point support. Quite common in financial software anyway, using integers to represent value requires stepping down to small units, in order to allow for sufficient precision. The simple example is expressing values in cents rather than dollars, as it would be impossible to represent $0.5 otherwise. In fact, smart contracts usually step down much further with 18 decimal places being supported by many tokens.
One issue developers are quite aware of nowadays is the fact that integers may overflow. Like mileage counters in cars, integers expressed in computers have a maximum value and once that value is reached they simply turn back to the beginning and start at the minimum value. Similarly, subtracting 4 from 3 in an unsigned integer will cause an underflow, resulting in a very large number. Developers are generally aware of this potential vulnerability and protect against it by using a safe math library, such as Open Zeppelin’s excellent implementation.
However, what many developers seem to fail to appreciate, is that integer arithmetic can generally lead to a lack of precision when done wrongly. In particular, the order of operations is important. A classic case is calculating percentages. For example, in order to calculate 25 percent, we typically divide by 100 and multiply by 25. Let’s say we wish to calculate 25 percent of 80 using only integers. Expressing this as 80 / 100 * 25 will result in 0, because of rounding errors. The simple mistake here is to perform the division before the multiplication.
The above is, of course, a very simple example, but we do come across integer arithmetic bugs with alarming frequency.
2. Block Gas Limit Vulnerabilities
The block gas limit is Ethereum’s way of ensuring blocks don’t grow too large. It simply means that blocks are limited in the amount of gas the transactions contained in them can consume. Put simply, if a transaction consumes too much gas it will never fit in a block and, therefore, will never be executed.
This can lead to a vulnerability that we come across quite frequently: If data is stored in variable-sized arrays and then accessed via loops over these arrays, the transaction may simply run out of gas and be reverted. This happens when the number of elements in the array grows large, so usually in production, rather than in testing. The fact that test data is often smaller makes this issue so dangerous since contracts with this issue usually pass unit tests and seem to work well with a small number of users. However, they fail just when a project gains momentum and the amount of data increases. It is not uncommon to end up with unretrievable funds if the loops are used to push out payments.
3. Missing Parameter or Precondition Checks
One of the simplest programming mistakes that we encounter is either not validating the arguments of a function or forgetting essential checks for an operation to be valid. Often, this includes address parameters not being checked for the zero address or, for example, not verifying that a user has sufficient token balance to execute a certain operation. Another good example is access control, where only a certain type of user should be authorized to call a function but this check is never performed.
These errors are usually the result of a sloppy design process. It is a good idea to have a written specification of all functions, stating the parameters, pre-conditions, and operations to be performed. Sticking to best practice design patterns, such as Checks Effects Interactions, can also help to prevent this type of vulnerability.
Potential frontrunning is probably the hardest issue to prevent on our list of common vulnerabilities. Frontrunnuing can be defined as overtaking an unconfirmed transaction. This is a result of the blockchain’s transparency property. All unconfirmed transactions are visible in the mempool before they are included in a block by a miner. Interested parties can simply monitor transactions for their content and overtake them by paying higher transaction fees. This can be automated easily and has become quite common in decentralized finance applications.
It is not easy to protect against frontrunning, and the issues we detect often require some significant refactoring or redesign, in order to be fixed.
5. Simple Logic Bugs
Some of the above issues are more specific to smart contracts, others are common to all types of programming. However, by far the most common type of issue we detect consists of simple mistakes in the logic of the smart contract. These errors may the result of a simple typo, a misunderstanding of the specification, or a larger programming mistake. They tend to have severy implications on the security and functionality of the smart contract.
What they all have in common though, is the fact that they can only be detected if the auditor understands the code base completely and has an insight into the project’s intended functionality and the contract’s specification. It is these types of issues that are the reason smart contract audits take time, are not cheap, and require highly experienced auditors.
Things have changed over the last couple of years in smart contract programming. Countless high profile cases resulting in lost money have made projects aware of the need to take security seriously. We do find that developers are more aware of common vulnerabilities and frequently employ tools, such as static code analysis and symbolic execution to automatically scan their code.
However, we have never performed an audit that has not resulted in at least one minor issue in the first round and plenty of really experienced developers have presented us with critical vulnerabilities in their code. The nature of the common issues is such that a full manual smart code audit is essential and this should be performed by an independent team with fresh eyes.
For inquiries on our smart contract and full-stack audits, please contact us at Cryptonics.