Common Smart Contract Vulnerabilities and How To Mitigate Them
Saturday, 20 October 2018 · 49 min read · solidity ethereumIn traditional software development, security vulnerabilities can be fixed by patching. When there’s a bug in your system, you can write a fix, deploy it, and prevent future exploits of that specific bug. Patches are frequent and easy.
Patching security vulnerabilities of decentralized applications on the Ethereum blockchain is not so straightforward. Due to the immutable nature of smart contracts, it’s difficult (and sometimes impossible) to upgrade already deployed contracts.
On the other hand, the potential losses of smart contract hacks can be exorbitant, with losses of more than US$70M for the DAO Hack and US$200M for the 2nd Parity Hack. Considering both the difficulty of upgrading contracts and high exploit risk, smart contract developers need to be vigilant and apply defensive programming techniques when designing smart contracts to prevent vulnerabilities in the initial design.
In this article, let’s look at common security vulnerabilities in Solidity smart contracts and how to mitigate them.
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.
This article assumes the reader has some familiarity with the Ethereum blockchain and the Solidity programming language.
Table of Contents
- All data is public
- Implicit visibility
- Integer underflow and overflow
- Reentrancy
- Denial of service
- Bad randomness
- Other vulnerabilities
Vulnerability: All data is public
In the Solidity programming language, you can set the visibility of state variables. They can be specified as public
, internal
, and private
. For example:
You can follow along by deploying this contract to the Rinkeby testnet via the Remix IDE.
Here is a comparison of the different visibility modifiers:
private
variables are visible exclusively in the contract they are defined in and not in derived contracts.internal
variables are visible in the contract they are defined in and in derived contracts. There are no getters generated for external consumption.public
variables are visible within the contract in which it’s defined, in derived contracts, and by external parties. An automatic getter function is generated that is publicly accessible.
It’s a common mistake to think that non-public
keywords allow you to store secrets on the blockchain. Making something private
or internal
only prevents other contracts from accessing and modifying the state variable. It is still visible to the whole world outside of the blockchain.
To access the contract storage , we can use the web3 method web3.eth.getStorageAt
:
The index
comes from the order of the variables in the contract. In our contract, health
, mana
, and secret
would have indices 0, 1, and 2 in that order.
web3
is a Javascript client for the Ethereum blockchain. It’s an API wrapper for the JSON-RPC API of Ethereum nodes.
Time to give this a try! Let’s retrieve the storage contents of a smart contract. With Metamask installed and pointing to the Rinkeby testnet, enter the following into your browser’s JS console:
You can use
0x0e8348bec2710fe2f996dd07758816ffed8be583
address of the examplePublicData
contract which I’ve deployed on Rinkeby.
We get the following results:
index | value (bytes32) | value |
---|---|---|
0 | 0x00000000000…0000064 | 100 |
1 | 0x00000000000…0000032 | 50 |
2 | 0x666f6f00000…0000006 | secret |
Values in the Ethereum state tree are stored as bytes32
- hex values which are 64 characters long, corresponding to 32 bytes of storage.
The value of index 0
is 0x64
, which is 100
in decimal. This matches the value of our first public variable, health
. Likewise for the mana
variable, 0x32
is 50
.
The value of index2
is a string
. Solidity uses Unicode UTF-8 to encode strings. Short strings fewer than 31 bytes long are stored in a single word. The last byte indicates the length (L) of the string in nibbles , while the string itself is stored in the first L nibbles. A nibble is half a byte, or one hexadecimal character.
Let’s take a look at our bytes32
value for secret
:
The last byte is 0x6
, so the string is 6 nibbles, or 3 bytes long. The first 6 nibbles of the word are 0x666f6f
, which decodes to the Unicode string foo
.
But that’s supposed to be a private
secret!
To sum up, all data on your Solidity smart contract is public.
The memory layout of more complex types are outside the scope of this article. Mappings are more complex to decode because its keys are not enumerable, but you can read historical transaction details and events to figure out present values. You can read about Solidity storage layouts to learn more.
Mitigation: All data is public
Remember that all data in your Solidity smart contracts is public.
Avoid writing any private secrets onto your contract. Like in version control, you can’t ‘remove’ already written values from the public eye either.
When you need private data
Applications such as on-chain games and auctions sometimes require submitted data to be private up until some point in time. Instead of publishing the raw value, use commit-reveal schemes instead.
For example, in an on-chain Rock-Paper-Scissors game, players submits a hash of their move, and only reveal (by submitting the move that matches the hash) once all players have committed their hashes.
When you need authorization
For authorization use cases, instead of using password strings use public-key cryptography instead. You can produce signed messages with ECDSA signatures which can be trusted by any party. Users can sign a message with their private key, which then gets verified by a contract using the user’s public key.
This is an increasing common design pattern in decentralized identity solutions such as ERC725 Identity and delegated meta transactions.
Vulnerability: Implicit visibility
Solidity contract functions can have one of public
, private
, internal
, or external
visibility modifiers.
If left unspecified, functions are public
by default and can be called by external users. This can prove disastrous if it’s a function that’s not meant for public consumption. For example, take a look at the following contract:
We have a private
function that’s not callable publicly. Within the function, we call a _sendTokens()
function that’s intended to be an internal function but has no visibility specified.
Because no visibility is explicitly defined for _sendTokens
, it’s publicly accessible! Anyone can call _sendTokens
function to send tokens, bypassing our private function.
Mitigation: Implicit visibility
Remember to define ALL visibility modifiers explicitly.
Use linters such as solhint
to help warn you of any implicit visibility, forcing you to explicitly define each function and state variable’s visibility modifier.
Vulnerability: Integer underflow and overflow
Integer data types in Solidity do not have built-in protection against underflow and overflow errors. For example:
Mitigation: Integer underflow and overflow
When writing contracts, use OpenZeppelin’s SafeMath
library for all uint
arithmetic. The SafeMath
contract checks for underflow and overflow by throwing an error if it spots one:
To use SafeMath
in your contracts, do:
Vulnerability: Reentrancy
A Reentrancy attack takes over control flow by calling the same contract function repeatedly. Take a look at the contract below:
The contract has two functions, deposit()
and withdraw()
.
Within the withdraw()
function, we subtract the user’s balance balances[msg.sender] -= amount
after we make an external transfer()
call.
Now let’s take a look at the malicious contract below. This contract is able to drain the Vault
of ether by calling withdraw()
over and over.
The expolit contract defines a hack()
function that deposit()
an arbitrary amount for us to be able to withdraw()
later. To begin the attack, we call Vault.withdraw()
.
When Vault.withdraw()
calls msg.sender.transfer()
, the transfer()
destination can be either an Externally Owned Address (wallet) or a contract address.
If the destination is a contract, a transfer will trigger any payable
fallback functions defined in the destination contract. Out exploit defines a fallback function that makes another call to Vault.withdraw()
. Within the same transaction, our exploit contract can re-enter the function by calling withdraw()
yet again before any amount is subtracted from our balance.
The above diagram illustrates the recursive loop that lets an attacker drain all funds from the
Vault
contract.
Because the balance is not yet subtracted, the Vault
’s require()
check does not behave as you would expect. The Vault
repeatedly triggers the malicious contract’s fallback function. The attackers then repeatedly withdraw and drain the contract of ether.
In summary, the vulnerability is caused primarily by not deferring unstrusted external contract calls until after any state modifications are applied later in the function.
Mitigation: Reentrancy
Use Checks-Effects-Interactions
You can use the Checks-Effects-Interactions pattern to reduce the attack surface for malicious contracts trying to hijack control flow after an external call.
According to the pattern:
- Checks in the beginning assure that the calling entity is in the position to call this particular function (e.g. has enough funds).
- Afterwards all specified Effects are applied and the state variables are updated.
- Only after the internal state is fully up to date, external Interactions should be carried out.
In other words, make sure you don’t call an external function until you’ve done all the internal work you need to do:
The Checks-Effects-Interactions pattern can be counterintuitive, but this new version of the withdraw
function is no longer vulnerable to re-entrancy attacks.
Use Reentrancy Guard
You can also use a reentrancy guard to wrap around your functions:
A counter ensures that only one call is made to the wrapped function in a single transaction. To use a reentrancy guard, apply the nonReentrant
modifier:
The use of a counter lets you ensure that no reentrant calls are made during the same function invocation.
Vulnerability: Denial of Service
Untrusted external contract calls can cause re-entrancy attacks that drain your ether, but they can also stop a system in its tracks. Take for example the following contract:
When Auction.bid()
tries to refund the old leader via send()
, it reverts if the refund fails. At first sight this behaviour seems reasonable.
However, this means that a malicious bidder can become the leader while making sure that any refunds to their address will always fail. This can be achieved with the following fallback function:`
In this way, an attacker can prevent anyone else from calling the bid() function, and stay the leader forever.
A Denial of Service can also happen when making multiple push payments to untrusted parties. For example:
In the contract above, we make transfers to multiple recipients with .send()
. Since the destinations are untrusted, they could be contracts with a fallback function.
Just one malicious recipient that reverts will block the entire multiSend()
operation, preventing all other recipients from receiving payment!
Given enough refund addresses in the list the for-loop enumeration might also hit the block gas limit and revert the operation - making it inoperable. This can happen without any malicious parties. Be wary of for loops of appendable data structures in Solidity!
Mitigation: Denial of Service
Making external contract calls to untrusted parties are dangerous. Be sure to avoid this where possible and mark them in your code when absolutely necessary.
For payments use cases, pull payments are preferable to push payments for this reason. Instead of sending a payment to an unstrusted party, deposit the funds in an Escrow
that they can withdraw from:
Users would call this contract to withdraw their tokens independently.
Vulnerability: Bad randomness
Because transactions on the Ethereum blockchain are deterministic, there is no source of entropy or randomness in Ethereum.
A common pitfall is to use future block variables, that is, variables containing information about the transaction block whose value is not yet known, such as hashes, timestamps, blocknumber or gas limit.
However, block variables are 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.
Take a look of the following contract (from Ethernaut level 3):
In the above contract, CoinFlip.flip()
gives you a win if you manage to guess the outcome of a random number generation based on block.blockhash
. We can exploit the deterministic nature of block variables by defining a new contract that copy and pastes the random number generation:
By making sure the exploit transaction gets mined in the same block as the legitimate transaction, the parent blockhash will be the same and consequently the generated random number is accessible to our Exploit
contract. With this in mind, we’re able to win the coin flip every time.
In summary, Solidity does not have a built-in source of entropy that can be used to generate random numbers.
Mitigation: Bad randomness
Avoid using block variables as a source of entropy.
The only way to maintain the randomness of the source is to use a two-transaction system:
- The first transaction locks in a future block number as the source of entropy.
- After that selected block has been mined, the second transaction uses that block’s blockhash as the source of entropy to run its logic.
Existing gambling DApps make use commit-reveal schemes such as RANDAO or a randomness oracle that injects entropy from an external API.
PRNG is a fairly big topic, and will be expanded further in a future blog post.
Other Vulnerabilities
Here are a few other vulnerabilities that you should also keep in mind:
- Use
msg.sender
instead oftx.origin
, astx.origin
makes your contracts vulnerable to phishing attacks. - Using
block.timestamp
for time-sensitive logic should tolerate up to 600 seconds of variation as it can be influenced by miners’ actions. - Be careful doing floating point arithmetic, because Solidity has no floats. (Call multiplication before division.)
delegatecall
lets you to modularize your code and build upgrade mechanisms, but can be dangerous if used improperly.- Ether can be forcibly be sent to a contract without triggering its fallback function, using
selfdestruct()
. Avoid checking invariants that depend on a contract’s ether supply. - The Smart Contract Weakness Classification Registry offers a complete and up-to-date catalogue of known smart contract vulnerabilities and anti-patterns along with real-world examples. Browsing the registry is a good way of keeping up-to-date with the latest attacks.
Security Static Analysis Tools
Here are several open source tools that can help you identify vulnerabilities in your smart contracts:
- solhint: Solidity linter. This project provides both Security and Style Guide validations.
- mythril: An open-source security analysis tool for Ethereum smart contracts. It uses concolic analysis, taint analysis and control flow checking to detect a variety of security vulnerabilities.
I highly recommend setting up solhint
at the very minimum. It helps warn you of many security vulnerabilities such as reentrancy and more.
General Security Principles
Ethereum and smart contracts are new and highly experimental. Therefore, you should expect constant changes in the security landscape, as new bugs and security risks are discovered, and new best practices are developed.
Being familiar with the security vulnerabilities mentioned in this article is just the start. Here are a few things you’ll need to do to be a successful smart contracts developer:
- Be aware of common security vulnerabilities, such as the ones mentioned in this article.
- Set up static analysis tools to help you spot any potential exploits in your contracts.
- Use open source, vetted smart contracts and reference implementation where possible.
- Beyond gaining awareness of common vulnerabilities and setting up static analysis tools, it’s also critical to have an external auditor examine your code for any bugs you might’ve missed.
If you’re interested to learn more about Solidity smart contract security best practices, check out the DASP Top 10 and Consensys best practices for more vulnerabilities and recommendations.
Summary
In this article, we’ve looked at common smart contract vulnerabilities and how you can mitigate them. Due to the immutable nature of smart contracts, defensive upfront design is absolutely critical to iron out any security vulnerabilities.
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.