“The history of smart contracts is really the history of smart contract bugs.”
– Aaron Blankstein, Blockstack PBC
If you’ve heard of smart contracts, you’ve likely also heard about some of their notable failures. As more assets are created and stored in smart contracts, their vulnerabilities become more consequential as the prize for exploiting them grows. As a point of reference, assets locked up in smart contracts crossed over $1 Billion in early February 2020, having grown from $700 Million in December 2019.
A quick Google search will lead you to results like:
- Blockchain Smart Contracts: More Trouble Than They Are Worth?
- 25% of All Smart Contracts Contain Critical Bugs
- Seven reasons you should worry about smart contracts
- Smart contract hacks cost millions
In this post, we’ll highlight 8 of the most common smart contract vulnerabilities identified by the NCC Group, a global expert in cybersecurity and risk mitigation (and a provider Blockstack PBC has used repeatedly to audit Blockstack Core). We’ll break down the where smart contracts have gone wrong or been taken advantage of, provide some real-world examples, and then describe how Clarity, the smart contracting language Blockstack engineers are developing, is designed to prevent them.
If you’re not familiar with Clarity, take a look at this introductory post for an overview covering what makes Clarity unique. For developers, please see the documentation here and try Clarity smart contracts today.
“Reentrancy occurs when external contract calls are allowed to make new calls to the calling contract before the initial execution is complete. For a function, this means that the contract state may change in the middle of its execution as a result of a call to an untrusted contract or the use of a low level function with an external address.” – DASP
Long story short, an attacker can repeatedly call into the contract from the same transaction, and in doing so, corrupt its internal state. This opens the contract to a number of risks and in some cases, the entire token balance could be drained as happened when the DAO was famously hacked. Eventually, it required a contentious hard fork of the underlying chain to remedy the situation.
Clarity: Clarity doesn’t allow reentrancy, period. Clarity design takes the position that reentrancy is an anti-feature and shouldn’t be allowed in a secure smart contract system. Given the issues in Ethereum smart contracts, Solidity now has a
noReentrancy guard you can attach to your public functions, but it is only provided on an opt-in basis.
2. Access Control
“Access Control issues are common in all programs, not just smart contracts. In fact, it’s number 5 on the OWASP top 10. One usually accesses a contract’s functionality through its public or external functions. While insecure visibility settings give attackers straightforward ways to access a contract’s private values or logic, access control bypasses are sometimes more subtle. These vulnerabilities can occur when contracts use the deprecated
tx.origin to validate callers, handle large authorization logic with lengthy
require and make reckless use of
delegatecall in proxy libraries or proxy contracts.” – DASP
Example: The Parity wallet situation is likely the most well-known occurrence of this bug, having frozen approximately $28 Million in ETH.
Clarity: Visibility isn’t a problem when using Clarity because everything is private by default. Only functions declared as
define-public can be accessed by other contracts and transactions. Going a step further, the Parity multisig bug was due to the fact that Solidity contracts have a fall-back function that gets called if a contract function call doesn’t match any existing function. Clarity does not have this by design so there is no way to mine a transaction that calls into a non-existent function.
3. Overflow and Underflow
“Integer overflows and underflows are not a new class of vulnerability, but they are especially dangerous in smart contracts, where unsigned integers are prevalent and most developers are used to simple
int types (which are often just signed integers). If overflows occur, many benign-seeming codepaths become vectors for theft or denial of service.” – DASP
Example: You may have heard of BEC Token which experienced this vulnerability when an attacker was able to steal huge amounts of ETH by exploiting a numeric overflow bug. This resulted in trading of BEC Token being suspended and researchers identifying that at least a dozen other ERC-20 tokens were susceptible to the same attack.
Clarity: Numeric overflows and underflows cause the transaction to abort in Clarity. Also, Clarity has a first-class type for a fungible token with a fixed supply, which prevents the supply from accidentally being inflated (a transaction that tries to mint more tokens than can exist will be forced to abort).
4. Unchecked Return Values For Low Level Calls
“One of the deeper features of Solidity are the low level functions
send(). Their behavior in accounting for errors is quite different from other Solidity functions, as they will not propagate (or bubble up) and will not lead to a total reversion of the current execution. Instead, they will return a boolean value set to false, and the code will continue to run. This can surprise developers and, if the return value of such low-level calls are not checked, can lead to fail-opens and other unwanted outcomes.” – DASP
Example: King of Ether and EtherPot are given by DASP as examples of this vulnerability and while they didn’t result in large amounts of money lost, forgetting to check the return value of
send() caused the contracts to misbehave.
Clarity: Clarity addresses this in its type system. All publicly-callable functions in Clarity must return a
(response a b) type, which encodes either an “everything is fine” type
(ok a) or a “something went wrong” type
(err b). This is enforced by the consensus rules — you cannot create a public function that does not do this. At the same time, the function’s caller must explicitly handle the
err cases in order to get the returned value of the function — their code won’t be instantiated if it doesn’t (so you can’t just ignore errors). In addition, if the top-level public function returns an
(err b) response, the transaction aborts (so unhandled errors will cause the transaction to abort). While this doesn’t prevent buggy error handling, it does prevent accidental omission of it.
5. Denial of Service
“Denial of service is deadly in the world of Ethereum: while other types of applications can eventually recover, smart contracts can be taken offline forever by just one of these attacks. Many ways lead to denials of service, including maliciously behaving when being the recipient of a transaction, artificially increasing the gas necessary to compute a function, abusing access controls to access private components of smart contracts, taking advantage of mixups and negligence, etc. This class of attack includes many different variants and will probably see a lot of development in the years to come.” – DASP
Example: A classic example is GovernMetal, a ponzi scheme where funds were trapped because the game became so successful that “arrays grew so large that the gas needed to clear them was more than the maximum allowed for a single transaction. The end result was a permanent freeze of the jackpot payout.”
Clarity: Clarity’s analysis system will tell you how expensive each possible code execution path will be. This is because the maximal memory usage and execution path lengths of each possible code branch are determined when the contract is instantiated. This means that a contract’s space and time resource requirements do not grow unbound in Clarity.
6. Bad Randomness
“Randomness is hard to get right in Ethereum. While Solidity offers functions and variables that can access apparently hard-to-predict values, they are generally either more public than they seem or subject to miners’ influence. Because these sources of randomness are to an extent predictable, malicious users can generally replicate it and attack the function relying on its unpredictablility.” – DASP
Clarity: Clarity has access to the Stacks blockchain’s verifiable random function, which can be used to seed a deterministic random number generator within the contract. The act of biasing the VRF, while possible, is exceedingly difficult because it requires attacking the Bitcoin chain itself.
7. Time manipulation
“From locking a token sale to unlocking funds at a specific time for a game, contracts sometimes need to rely on the current time. This is usually done via
block.timestamp or its alias
now in Solidity. But where does that value come from? From the miners! Because a transaction’s miner has leeway in reporting the time at which the mining occurred, good smart contracts will avoid relying strongly on the time advertised.” – DASP
Clarity: Clarity exposes Bitcoin’s block timestamp as the Stacks block timestamp. This doesn’t fix the vulnerability outright, but it does make attacking Clarity’s view of time tantamount to attacking Bitcoin’s view of time.
8. Short address attack
“Short address attacks are a side-effect of the EVM itself accepting incorrectly padded arguments. Attackers can exploit this by using specially-crafted addresses to make poorly coded clients encode arguments incorrectly before including them in transactions.” – DASP
Clarity: Clarity doesn’t do padding in the first place. All Clarity data is strongly-typed, so there’s no way to do a “buffer overflow”-like attack such as this.
More: Unknown Unknowns
“Ethereum is still in its infancy. The main language used to develop smart contracts, Solidity, has yet to reach a stable version and the ecosystem’s tools are still experimental. Some of the most damaging smart contract vulnerabilities surprised everyone, and there is no reason to believe there will not be another one that will be equally unexpected or equally destructive.” – DASP
Clarity: Clarity has post-conditions on tokens, which allows users to proactively defend their assets from theft or destruction by unknown attackers and unknown bugs in other contracts. Also, in Clarity, the set of reachable code can be efficiently determined, so you can ensure that your transaction only ever runs code you have vetted yourself first.
In summary, a key underlying principle of Clarity is ‘what you see is what you get’. As a decidable language, Clarity enables developers to know, with certainty, from the code itself what the program will do. Clarity is also intentionally Turing incomplete and avoids “Turing complexity.” This allows for complete static analysis of the entire call graph of a given smart contract.
Clarity’s support for types and type checker can eliminate whole classes of bugs like unintended casts, reentrancy bugs, reads of uninitialized values, and types like those outlined above. Finally, you can analyze Clarity code for runtime cost and data usage. This empowers developers to predict what a given Clarity program will do, and how much it will cost.
In addition to being a decidable language, Clarity is also interpreted. The contract source code itself is published and executed by blockchain nodes, thereby removing any intermediate, compiled representation (e.g., EVM byte code for Solidity) and further minimizing the surface area for introducing bugs. Publishing the contract source code also optimizes understandability. Compiler bugs are doubly damaging in blockchains because while the programmed source code may not have an error, the eventual program reaching the blockchain could have errors. Any such errors would require contentious hard forks — which are potentially infeasible — to remedy.
You can work with the developer preview of Clarity here.