In 2018, we performed our initial research about the current state of security in the context of Smart Contracts, focusing on those written in Solidity "a contract-oriented, high-level language for implementing smart contracts". At that time, we compiled a Top 10 list of the most common Smart Contracts security issues based on publicly available Smart Contracts source code. The time has come to update that research and evaluate how Smart Contracts security has evolved since then. Although Top 10 lists are nice to have, they tend to not highlight additional interesting details, since some of the details don’t exactly align with the Top 10 list. Before digging into the updated Smart Contracts Top 10 list, here are some highlights from our original research:
- In 2018, Denial-of-Service by External Contract, and Reentrancy, were the top 2 issues. However, the issues were are now remedied. You can learn more about Reentrancy in our recent research blog: Checkmarx Research: Solidity and Smart Contracts from a Security Standpoint. When Solidity v0.6.x was released that introduced a lot of breaking changes, 50% of scanned Smart Contracts were not even ready for Solidity compiler v0.5.0. This becomes even more relevant since 30% of the Smart Contracts use deprecated constructions (e.g., sha3, throw constant, etc.), and 83% had issues with the compiler version specification (pragma).
- Although visibility issues didn't make it in the 2018 Top 10 list, nor in this updated version, they have increased by 48%. You can read more about this issue in our previous blog and the official documentation.
Solidity Top 10 Common Issues
S1 - Unchecked External CallThis was the third most-common issue on our previous Top 10 list. Since the top 2 issues are now resolved, Unchecked External Call has moved up to the most common issue in the 2020 updated list. Solidity low-level call methods (e.g., address.call()) do not throw an exception. Instead, they return false if the call encounters an exception. On the other hand, contract calls (e.g., ExternalContract.doSomething()) automatically propagate a throw if doSomething() throws. Transferring Ether using addr.send()is a good example where unsuccessful transfers should be handled explicitly by checking the return value, but this is also valid for other external calls.
S2 - Costly LoopsCostly loops moved from forth on the Top 10 list to second. Despite the fact that the top 2 issues from our previous list are resolved, the number of affected Smart Contracts increased by almost 30%. Computational power on Ethereum environments is paid (using Ether). Thus, reducing the computational steps required to complete an operation is not only a matter of optimization, but also cost efficiency. Loops are a great example of costly operations: as many elements an array has, more iterations will be required to complete the loop. As you may expect, infinite loops exhaust all available gas. If an attacker is able to influence the elements array length, then they will be able to cause a denial of service, preventing the execution to jump out of the loop. Although it was far from the Top 10 common issues, array length manipulation was found in 8% of the scanned Smart Contracts.
S3 - Overpowered OwnerThis is a new entry in the Top 10 list, affecting approximately 16% of the scanned Smart Contracts. Some contracts are tightly coupled to their owner, making some functions callable only by the owners address, as in the example below. Both doSomething() and doSomethingElse() functions can only be called by the contract owner: the former uses the onlyOwner modifier, while the later enforces it explicitly. This poses a serious risk: if the private key of the owner gets compromised, then an attacker can gain control over the contract.
S4 - Arithmetic PrecisionSolidity data types are cumbersome due to the 256 bits Virtual Machine (EVM). The language does not offer a floating point representation, and data types shorter than 32 bytes are packed together into the same 32 bytes slot. With this in mind, you should expect precision issues. When division is performed before the multiplication, as in the example above, you should expect huge rounding errors.
S5 - Relying on tx.originContracts should not rely on tx.origin for authentication, since a malicious contract may play in the middle, draining all the funds: msg.sender should be used instead. You’ll find a detailed explanation of Tx Origin Attacks on Solidity’s documentation. Long story short, tx.origin is always the first account in the call chain, while msg.sender is the immediate caller. If the last contract in the chain relies on tx.origin for authentication, then the contract in the middle will be able to drain the funds, since no validation is performed on who’s calling (msg.sender).
S6 - Overflow / UnderflowSolidity’s 256 bits Virtual Machine (EVM) brought back overflow and underflow issues as demonstrated here. Developers should be extra careful when using uint data types in for-loop condition, since it may result in infinite loops. In the example above, the next value for i when its value is 0 will be 2256-1, that makes the condition always true. Developers should prefer <, >, != and == for comparison.
S7 - Unsafe Type InferenceThis issue moved up two positions, now affecting more than 17% of Smart Contracts then before. Solidity supports Type Inference, but there are some quirks with it. For example, the literal 0 type-infers to byte, not int as we might expect. In the example below, the type of i is inferred to uint8: the smallest integer type sufficient to store the right-hand side value. If elements has more than 256 elements, we should expect an overflow. Explicitly declaring data types is recommended to avoid unexpected behaviors and/or errors.
S8 - Improper TransferThis issue dropped from sixth to eighth in the Top 10 list, affecting now less than 1% of the scanned Smart Contracts. There is more than one way to transfer Ether between contracts. Although calling the addr.transfer(x) function is the recommended way, we still found contracts using send() function instead. Note that addr.transfer(x) automatically throws an exception if the transfer is unsuccessful, mitigating the Unchecked External Call issues previously discussed: S1.
S9 - In-Loop TransfersWhen Ether is transferred in a loop, if one of the contracts cannot receive it, then the whole transaction will be reverted. An attacker may take advantage of this behavior to cause a denial-of-service, preventing other contracts to receive Ether.
S10 - Timestamp dependenceThis was fifth in the previous version of the Top 10 list. It’s important to remember that Smart Contracts run on multiple nodes on a different time. The Ethereum Virtual Machine (EVM) does not provide clock time and the now variable, commonly used to obtain a timestamp, is in fact an environment variable (an alias of block.timestamp) which miners can manipulate. Since miners can manipulate environment variables currently, its value should only be used in inequalities >, <, >=, and <=. When looking for randomness, consider the RANDAO contract, which is based on a Decentralized Autonomous Organization (DAO) that anyone can participate in, being the random number generated by all participants together.
ConclusionWhen comparing the 2018 and 2020 Top 10 Common Issues lists, we can observe some progress concerning development best practices, especially those impacting security. Seeing the 2018 top 2 issues, Denial-of-Service by External Contract, and Reentrancy, moving away from the top 10 is a positive sign, but there’s still important steps to take to avoid common mistakes. Remember that Smart Contracts are immutable by design, meaning that once created, there’s no way to patch the source code. This poses a great challenge concerning security and developers should take advantage of the available application security testing tools to ensure source code is well-tested and audited before deployment. Solidity is a very recent programming language that is still maturing. Solidity v0.6.0 introduced a few breaking changes and more are expected in the upcoming versions. Discovering issues and risks like the ones mentioned herein is why the Checkmarx Security Research team performs investigations. This type of research activity is part of their ongoing efforts to drive the necessary changes in software security practices among organizations worldwide.
- Checkmarx Research: Solidity and Smart Contracts from a Security Standpoint
- Solidity official documentation
- Solidity Security Considerations: tx.origin
- Type Inference – Wikipedia
- RANDAO: A DAO working as RNG of Ethereum
- SmartCheck: Static Analysis of Ethereum Smart Contracts
- Recommendations for Smart Contract Security in Solidity