Common Smart Contract Vulnerabilities and How To Mitigate Them

In 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

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:

pragma solidity 0.4.25;

contract PublicData {
    uint public health = 100;
    uint internal mana = 50;
    string private secret = "foo";
}

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:

web3.eth.getStorageAt(contractAddress, index)

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:

> web3.eth.getStorageAt("0x0e8348bec2710fe2f996dd07758816ffed8be583", 0, function(error, result){
   if(!error)
       console.log(JSON.stringify(result));
   else
       console.error(error);
})

You can use 0x0e8348bec2710fe2f996dd07758816ffed8be583 address of the example PublicData 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:

0x666f6f0000000000000000000000000000000000000000000000000000000006

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:

contract Vault {
  function disburse() private { // This is private
    _sendTokens(msg.sender)
  }

  function _sendTokens() { // This is public by default!
    ...
  }
}

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:

uint foo = 1;
foo = foo - 2; // Underflow! 2^256 -1

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:

pragma solidity ^0.4.24;

/**
 * @title SafeMath
 * @dev Math operations with safety checks that revert on error
 */
library SafeMath {

  /**
  * @dev Multiplies two numbers, reverts on overflow.
  */
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }

    uint256 c = a * b;
    require(c / a == b);

    return c;
  }

  /**
  * @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
  */
  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b > 0); // Solidity only automatically asserts when dividing by 0
    uint256 c = a / b;

    return c;
  }

  /**
  * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
  */
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b <= a);
    uint256 c = a - b;

    return c;
  }

  /**
  * @dev Adds two numbers, reverts on overflow.
  */
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c >= a);

    return c;
  }

  /**
  * @dev Divides two numbers and returns the remainder (unsigned integer modulo),
  * reverts when dividing by zero.
  */
  function mod(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b != 0);
    return a % b;
  }
}

To use SafeMath in your contracts, do:

pragma solidity 0.4.25;

import "openzeppelin-solidity/contracts/math/SafeMath.sol";

contract MyContract {
    using SafeMath for uint;

    ... do any uint arithmetic you want
}

Vulnerability: Reentrancy

A Reentrancy attack takes over control flow by calling the same contract function repeatedly. Take a look at the contract below:

contract Vault {
    mapping(address => uint) balances;

    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);
        msg.sender.transfer(amount); // At this point, the caller's code is executed and calls withdraw again!
        balances[msg.sender] -= amount;
    }
}

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.

contract ReentrancyAttack {
    Vault public vault;
    uint amount public = 100000000000;

    constructor(address vaultAddress) {
        vault = Vault(vaultAddress);
    }

    function hack() external {
      vault.deposit(amount);
    }

    function() payable { // Fallback function
      if (vault.balance != 0 ) {
        vault.withdraw(amount); // Re-enter the Vault
      }
    }
}

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:

function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount); // 1. Checks
    balances[msg.sender] -= amount; // 2. Effects
    msg.sender.transfer(amount); // 3. Interactions (external contract calls)
}

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:

pragma solidity ^0.4.24;

contract ReentrancyGuard {
  uint256 private _guardCounter;

  constructor() internal {
    // The counter starts at one to prevent changing it from zero to a non-zero value, which is a more expensive operation.
    _guardCounter = 1;
  }

  /**
   * @dev Prevents a contract from calling itself, directly or indirectly.
   * Calling a `nonReentrant` function from another `nonReentrant`
   * function is not supported. It is possible to prevent this from happening
   * by making the `nonReentrant` function external, and make it call a
   * `private` function that does the actual work.
   */
  modifier nonReentrant() {
    _guardCounter += 1;
    uint256 localCounter = _guardCounter;
    _;
    require(localCounter == _guardCounter);
  }
}

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:

contract MyContract is ReentrancyGuard {
  function test() external nonReentrant {
    ...
  }
}

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:

contract Auction {
    address currentLeader;
    uint highestBid;

    function bid() payable {
        require(msg.value > highestBid);

        require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert

        currentLeader = msg.sender;
        highestBid = msg.value;
    }
}

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:`

contract DenialOfService {
  function () payable {
    require(false); // reverts
  }
}

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:

address[] private refundAddresses;
mapping (address => uint) public refunds;

function multiSend() public {
    for(uint x; x < refundAddresses.length; x++) {
        require(refundAddresses[x].send(refunds[refundAddresses[x]]))
    }
}

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:

contract Escrow is {
  using SafeMath for uint256;

  event Deposited(address indexed payee, uint256 weiAmount);
  event Withdrawn(address indexed payee, uint256 weiAmount);

  mapping(address => uint256) private _deposits;

  function depositsOf(address payee) public view returns (uint256) {
    return _deposits[payee];
  }

  /**
  * @dev Stores the sent amount as credit to be withdrawn.
  * @param payee The destination address of the funds.
  */
  function deposit(address payee) public onlyPrimary payable {
    uint256 amount = msg.value;
    _deposits[payee] = _deposits[payee].add(amount);

    emit Deposited(payee, amount);
  }

  /**
  * @dev Withdraw accumulated balance for a payee.
  * @param payee The address whose funds will be withdrawn and transferred to.
  */
  function withdraw(address payee) public onlyPrimary {
    uint256 payment = _deposits[payee];

    _deposits[payee] = 0;

    payee.transfer(payment);

    emit Withdrawn(payee, payment);
  }
}

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):

pragma solidity ^0.4.18;

contract CoinFlip {
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function CoinFlip() public {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(block.blockhash(block.number-1));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

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:

contract Exploit {
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function hackFlip(bool _guess) public {
    uint256 blockValue = uint256(block.blockhash(block.number-1));
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
        originalContract.flip(_guess);
    } else {
        originalContract.flip(!_guess);
    }
  }
}

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 of tx.origin, as tx.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.