Blockchains are replicated, decentralized shared ledger. Smart contracts are digital protocols that allow us to work with digital assets and allow executing code without third parties on the blockchain. There are public blockchains and private blockchains and while both of them can have smart contracts, in this blog we will focus on ethereum, a public blockchain, because it is the most popular platform for writing smart contracts and building decentralized applications.
Blockchains enable us to create smart contracts that can run and function in a decentralized and trustless manner. The potential advantages of creating trustless systems using smart contracts are enormous, and as a result, we are seeing a boom in smart contract-based decentralized application development.
But as it is with any new technology, the security of these systems is not well understood. With smart contracts, security consciousness is extremely important as they deal with high-value digital assets. In June 2016, a three-line bug in a smart contract allowed an attacker to steal 150 million USD. This was the infamous DAO hack which was caused by a reentrancy bug, a surprisingly common class of bugs, which are now well-documented.
Ethereum
Ethereum is the most popular public blockchain framework for building smart contracts and decentralized applications. Ethereum allows developers to do this by building what is essentially an abstract foundational layer: a blockchain with a built-in Turing-complete programming language (called solidity), allowing anyone to write smart contracts where they can create their own arbitrary rules for ownership, transaction formats, and state transition functions.
When it was first introduced, ethereum opened up doors for developers in a revolutionary new way. They could create the first generation of decentralized applications, play around with bleeding edge technologies, create cryptocurrency-based products, and create customized currencies of their own demands.
In this article, we will dig deeper into the different types of bugs that exist in the smart contracts and the design patterns we can follow to avoid them. This article assumes a basic knowledge of blockchains, so if you’re not familiar with them here’s an excellent introduction.
Smart Contracts in Ethereum
It’s helpful to think of the ethereum blockchain as a state-transition system, where each transaction triggers a state transition. For example, in the figure below, a transaction of 10 ETH is sent from account 14c5f88a to account bb75a980. We also send some additional data, since the account is a contract account, and we want it to work on the data we’re sending.
As processing data and running functions require computational resources, we need to pay for this service with a quantity called gas, which is derived from the ETH cryptocurrency. Gas pays the miner in proportion to the resources the function call needed to execute fully. If the allocated gas runs out before the execution finishes, the state reverts to the previous one and it seems like nothing ever changed.
Here is a simplified step-by-step description of what would happen if a transaction like the one in the figure above were to execute:
- Check if the transaction is well-formed (i.e., has the right number of values), the signature is valid. If not, return an error.
- Calculate the transaction fee as STARTGAS * GASPRICE, and determine the sending address from the signature. Subtract the fee from the sender’s account balance and increment the sender’s nonce. If there is not enough balance to spend, return an error.
- Initialize GAS = STARTGAS, and take off a certain quantity of gas per byte to pay for the bytes in the transaction.
- Transfer the transaction value from the sender’s account to the receiving account. If the receiving account does not yet exist, create it. If the receiving account is a contract, run the contract’s code either to completion or until the execution runs out of gas.
- If the value transfer failed because the sender did not have enough money, or the code execution ran out of gas, revert all state changes except the payment of the fees, and add the fees to the miner’s account.
- Otherwise, refund the fees for all remaining gas to the sender and send the fees paid for gas consumed to the miner.
Now that we’ve got the basics out of the way, let’s dig into some recommended practices for writing safe smart contracts!
Smart Contract Security
Keep it Simple and Modular
Whenever designing complex systems, it always helps to keep your code small, modular, and understandable. This means following general recommendations about maintaining the quality of code, using descriptive variable and function names, separating concerns in different contract classes, and doing basic error-handling. It also helps to limit the number of local variables, the length of functions, and so on. Document your functions so that others can see what your intention was and whether it is different than what the code does.
Beware of Native Sources of Randomness
Sources of randomness are often needed when creating multi-party interactions in smart contracts, usually to “randomly” assign a role to a participant of the smart contract. But the problem is that the sources of randomness that are currently available natively in ethereum are predictable and hence vulnerable to attack. This problem is compounded because most developers often overlook the fact that weak sources of randomness can be one of the easiest ways to break a cryptographic system.
Arseny Reutov recently tried to predict random numbers in ethereum smart contracts and his team’s efforts were met with considerable success. The best workaround is to use your own implementation of a random number generator, which is a non-trivial task for most developers.
Beware When You Use the send() Function
Anytime you are using the send() function, you must take care of multiple things. If the function is sending ETH to another contract, make sure that there’s no possibility of the transaction failing silently or of the user’s gas running out due to a looping construct. This leads to your ETH being stuck in another contract which you may or may not be in control of. You can prevent this from happening by doing proper error-handling and writing reasonable tests before deploying your contract on the main net.
Anytime your contract is working with payable functions, it is advised that you follow the standard paradigm of “pull over push”. This is a bit abstract to understand without actually seeing it in practice, so here’s a great example that you can see and contrast to understand the difference between the two mindsets. In a nutshell, it’s the idea that a utility must be called when it’s needed and only when it’s needed, and preferably by the user and not an indirect call through another utility.
Write Tests
Most people prefer truffle suite for writing tests for solc contracts. Writing effective tests requires you to understand the use-case well, and list the most important assertions you want to test. A good practice is to start with the most basic assertion for payable functions and check if transactions are occurring, and then move onto bigger blocks of code. The more broken contracts you see online, and the more contracts you break, the better you will become at writing tests for solidity. It’s all about practice and experience.
Structure Your Code: Checks, Effects, Interactions
Most functions will first perform some checks (who called the function, are the arguments in range, was enough Ether sent, does the person have tokens, etc.). These checks should be done first. If all checks passed, effects to the state variables of the current contract should be made second. Interaction with other contracts should be the very last step in any function.
Early contracts delayed some effects and waited for external function calls to return in a non-error state. This simple structure saves us from a lots of unintended consequences, including problems like the reentrancy bug, running out of gas due to looping, getting stuck during contract calls, etc.
Formal Verification
Formal verification is a mathematical discipline which concerns itself with the act of proving or disproving the correctness of algorithms underlying a system, with respect to a certain formal specification or property. In the context of smart contracts, formal verification can help us explore and mathematically prove the set of all possible execution states a contract can lead to.
This is the highest form of security possible from a mathematical sense, since we are guaranteed that a contract won’t slip into a state that wasn’t in the set. Yoichi Hirai’s GitHub repository is a good place to start if you want to dig deeper into the tools techniques for formal verification of smart contracts.
Include a Fail-Safe Mode
Even if you took all the possible security measures possible, it’s still advised to include a fail-safe mechanism into your contract. You can add a function in your smart contract that performs some self-checks like “Has any Ether leaked?”, “Is the sum of the tokens equal to the balance of the contract?” or similar things.
If the self-check fails, the contract must automatically switches into some kind of “failsafe” mode, which, for example, disables most of the features, hands over control to a fixed and trusted third party, or defaults to just returning all the money back to senders. Remember that the fail-safe checks mustn’t consume too much gas themselves, so keep them short and simple.
The DAO Hack: A Case Study
DAO is an acronym for decentralized autonomous organization. It was an ambitious effort to create a stateless investor-directed venture capital fund on the blockchain. The DAO was crowdfunded in May 2016 and it set the record for the highest crowdfunding campaign in history. The very next month, USD 150 million was extracted from the contract by an attacker who exploited the reentrancy bug.
The DAO hack was a turning point for smart contract security. Not only because USD 150 million was stolen from a single smart contract, but because suddenly, academics started paying attention to smart contract security as a research field in its own right. The number of people interested increased, so did the number of reported bugs, and over time, ‘good practices’ standards for smart contracts started shaping up. In many ways, the DAO hack marked the beginning of this shift in attitude.
Before we go further, we must understand what reentrancy is. The following code snippet shows an example in which we first use the send() function and then set the value of the account to 0
An attacker can call withdraw recursively and the account balance of the contract would all drain into the attacker’s account. The account balance of the attacker would never be set to 0 because compilation might never reach the shares[msg.sender] step. It’s possible the gas would be exhausted by the recursive calls and the contract fails silently, meanwhile draining all the ETH in balance. Now let’s have a closer look at the actual code from the DAO code that the attacker exploited:
The basic idea was this: propose a split; execute the split. When the DAO goes to withdraw your reward, call the function to execute a split before that withdrawal finishes. The function will start running without updating your balance, and the line we marked above as “the attacker wants to run more than once” will run more than once.
This is the vulnerability that was used to drain USD 150 million from the DAO smart contract in June 2016. Smart contract security has come a long way since, but it still has a long way to go before these safety standards become more mainstream. The only thing we as developers can do, in the meanwhile, is to arm ourselves with information and keep our finger on the pulse for new bugs and vulnerabilities in the rapidly developing ecosystem of smart contracts.
References: