Smart contract security on Celo with Mythril

Smart contract security on Celo with Mythril https://celo.academy/uploads/default/optimized/2X/6/692b1f61cd94b947d0eb1d838631d18bfee2bdf2_2_1024x576.jpeg
none 5.0 1

Introduction

Smart contracts are a self-executing piece of code deployed on Ethereum-compatible blockchains like Celo free of censorship, downtime, and third-party interference, and have since birthed changed the way software is perceived. However, as exciting as smart contracts are, they introduce security risks ranging from low to critical ones. It then becomes crucial for developers to thoroughly scrutinize contract code before deployment.

Prerequisites​

To get the best from this guide, you need to have some knowledge about smart contracts. If you’re new to the field, I recommend going through this and this to get started.

Requirements​

For the purpose of this tutorial, the following are needed.

  • Ubuntu 22.04.02 LTS
  • Install an editor/IDE. VSCode recommended.
  • Install NodeJs version >=16.15.1.

What is Mythril?

Mythril is a security tool for analyzing smart Ethereum contracts. Note the emphasis - Ethereum. There are other smart contract platforms such as TRON and so on. But our focus is on the Celo blockchain which is highly compatible with Ethereum Virtual Machine. So if you write contracts that target the Celo platform, it is very important and helpful to imbibe the act of scrutinizing your code using security tools such as Mythril. This helps to detect and avoid a range of smart contract issues such as integer overflow/underflow, owner-overwrite-to-Ether-withdrawal, and many others.

Installation

To use this tool, you will need to perform the following setups. These steps are for those using the Windows operating system. If you already use Ubuntu, you may skip the steps that require its installation.

  • Set up the Linux distribution system

  • After step 1 is completed, you should have Ubuntu running on Windows OS. Launch the Ubuntu terminal. To get the best result, right-click on the icon to run as an administrator. Follow the instructions to set up a user account.

  • We need to install pip
  sudo apt install python3-pip

or

  sudo apt install pip
  • Run this command.
  sudo apt-get update
  • Check the version of pip3 installed
  pip3 --version
  • Install Mythril
  pip install mythril
  • Check if Mythril is installed
  myth version

If you have trouble viewing the version of Mythril, take the following steps.

  • Update Ubuntu
  sudo apt update
  sudo apt install software-properties-common
  sudo add-apt-repository ppa:ethereum/ethereum
  sudo apt install solc
  sudo apt install libssl-dev python3-dev python3-pip
  pip install mythril
  • Then, check the mythril version
  myth version

This should work fine. If you’re still having trouble setting up, please go over the steps again.

  • Next step, since you newly installed Ubuntu, you’re probably not going to have VSCode installed. In the Ubuntu terminal, run code .. This command triggers VSCode if already installed, otherwise, it will attempt to download the extension, so you should be connected to the internet.

Creating the project

  • Create root and project folders.
  mkdir myTutorial && cd myTutorial
  mkdir smart-contract-security-on-celo-with-mythril && cd smart-contract-security-on-celo-with-mythril
  • Create a hardhat project using yarn.
  yarn hardhat

If the command failed, you obviously do not have yarn installed. Follow the steps below to install yarn or skip if you already installed it.

  • Add yarn key
  curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
  echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
  sudo apt update && sudo apt install yarn

Then, check the version of the yarn.

  yarn --version

Alternatively, if the command failed, install yarn using npm otherwise skip this part. I recommend installing npm using nvm.

  • Install nvm
  curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash && source ~/.profile
  • Now, use NVM to install the desired version of npm
  nvm install 14.19.1
  • Check which current version of npm you’re using.
  nvm current
  • List all versions of npm installed.
  nvm list

Install Yarn using NPM globally

  npm install --global yarn
  • Check the version of the yarn. It should work fine.
  yarn --version

Invoke Hardhat

We can now use yarn to install hardhat. If everything is done correctly, running the next operations on Ubuntu is significantly faster than on Windows.

  yarn add hardhat
  npx hardhat

Follow the instructions to complete the project setup.

  • Navigate to the contracts folder. Create a new .sol file. I named mine VulnerableToken.sol. Then paste the code below.
// SPDX-License-Identifier: MIT
  pragma solidity 0.8.18;

  interface IERC20 {
    function transfer(address to, uint256 amount) external returns(bool);
    function allowance(address owner, address to) external returns(uint256);
    function transferFrom(address from, address to, uint amount) external returns(bool);
  }

  contract VulnerableToken {

    mapping (address => uint256) public tokenBalances;
    mapping (address => uint256) public balances;

    function withdraw() public {
      uint _balance = balances[msg.sender];
      (bool done,) = address(msg.sender).call{value: _balance}("");
      require(done);
      balances[msg.sender] -= _balance;
    }

    function depositToken(IERC20 token) public {
      uint allowance = IERC20(token).allowance(msg.sender, address(this));
      IERC20(token).transferFrom(msg.sender, address(this), allowance);
      tokenBalances[msg.sender] += allowance;
    }

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

    function withdrawToken(address _token) public {
      require(_token != address(0), "Token is zero address");
      uint tBalance = tokenBalances[msg.sender];
      if(tBalance > 0) {
        IERC20(_token).transfer(msg.sender, tBalance);
        tokenBalances[msg.sender] -= tBalance;
      }
    }
  }

The above code is a simple contract that allows any account to deposit and withdraw $Celo and ERC20 tokens. I have written the contract for the purpose of this tutorial.

Warning: Do not use the above code in production. It contains intended bugs meant for this guide.

Using Mythril

Let’s first analyze the Lock contract

  • Navigate to the project directory, and run the command
  myth analyze contracts/Lock.sol
  ==== Dependence on predictable environment variable ====
  SWC ID: 116
  Severity: Low
  Contract: Lock
  Function name: constructor
  PC address: 44
  Estimated Gas Usage: 369 - 3049
  A control flow decision is made based on The block. timestamp environment variable.
  The block.timestamp environment variable is used to determine a control flow decision. Note that the values of variables like coinbase, gaslimit, block number and timestamp are predictable and can be manipulated by a malicious miner. Also keep in mind that attackers know hashes of earlier blocks. Don't use any of those environment variables as sources of randomness and be aware that use of these variables introduces a certain level of trust into miners

From the description, Mythril alerted us of the possible dangers of using block timestamps. We are aware that miners can influence timestamps to an extent, even, if the hashes of the earlier blocks are known. The extent of the dangers this type of vulnerability can cause to the Lock contract is low, so it is a mild warning.

To solve this problem, you only have to ensure the level of dependency of your code on the on-chain timestamp is extremely low.

I have prepared another example contract in the second file Withdrawal.sol.

  myth analyze contracts/Withdrawal.sol

Running the command will invoke Mythril on the target contract. There can be many issues generated from a single function or variable, while Mythril has a default configuration of 3 transactions that can be returned, you can change the setting by using the -t flag followed by the number of transactions you want it to display.

Note: Pay attention that Mythril runs the contracts step by step taking one function at a time. This is what I’ve noticed over time. You might have to comment out the function that was already analyzed so Mythril can move to the next. This assertion may not be true if the issue has been fixed before you’re reading this.

  ==== External Call To User-Supplied Address ====
  SWC ID: 107
  Severity: Low
  Contract: Withdrawal
  Function name: withdrawToken(address)
  PC address: 940
  Estimated Gas Usage: 8957 - 64184
  A call to a user-supplied address is executed.
  An external message call to an address specified by the caller is executed. Note that the callee account might contain arbitrary code and could re-enter any function within this contract. Reentering the contract in an intermediate state may lead to unexpected behavior. Make sure that no state modifications are executed after this call and/or reentrancy guards are in place.
  --------------------
  In file: contracts/Withdrawal.sol:36

  IERC20(_token).transfer(msg.sender, tBalance)

  --------------------
  Initial State:

  Account: [CREATOR], balance: 0x0, nonce:0, storage:{}
  Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

  Transaction Sequence:

  Caller: [CREATOR], calldata: , decoded_data: , value: 0x0
  Caller: [ATTACKER], function: withdrawToken(address), txdata: 0x89476069000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef, decoded_data: ('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',), value: 0x0

  ==== State access after external call ====
  SWC ID: 107
  Severity: Medium
  Contract: Withdrawal
  Function name: withdrawToken(address)
  PC address: 1062
  Estimated Gas Usage: 8957 - 64184
  Read of persistent state following external call
  The contract account state is accessed after an external call to a user defined address. To prevent reentrancy issues, consider accessing the state only before the call, especially if the callee is untrusted. Alternatively, a reentrancy lock can be used to prevent untrusted callees from re-entering the contract in an intermediate state.
  --------------------
  In file: contracts/Withdrawal.sol:37

  tokenBalances[msg.sender] -= tBalance

  --------------------
  Initial State:

  Account: [CREATOR], balance: 0x0, nonce:0, storage:{}
  Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

  Transaction Sequence:

  Caller: [CREATOR], calldata: , decoded_data: , value: 0x0
  Caller: [ATTACKER], function: withdrawToken(address), txdata: 0x89476069000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef, decoded_data: ('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',), value: 0x0

  ==== State access after external call ====
  SWC ID: 107
  Severity: Medium
  Contract: Withdrawal
  Function name: withdrawToken(address)
  PC address: 1078
  Estimated Gas Usage: 8957 - 64184
  Write to persistent state following external call
  The contract account state is accessed after an external call to a user-defined address. To prevent reentrancy issues, consider accessing the state only before the call, especially if the callee is untrusted. Alternatively, a reentrancy lock can be used to prevent untrusted callees from re-entering the contract in an intermediate state.
  --------------------
  In file: contracts/Withdrawal.sol:37

  tokenBalances[msg.sender] -= tBalance

  --------------------
  Initial State:

  Account: [CREATOR], balance: 0x0, nonce:0, storage:{}
  Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

  Transaction Sequence:

  Caller: [CREATOR], calldata: , decoded_data: , value: 0x0
  Caller: [ATTACKER], function: withdrawToken(address), txdata: 0x89476069000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef, decoded_data: ('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',), value: 0x0

From the output, Mythril has found vulnerabilities and analyzed each of them as follows:

  • Type of Vulnerability.

  • Smart Contract Weakness ID - SWC ID.

  • Severity i.e extent of damage it can cause.

  • Contract Name.

  • The name of the function that was run.

  • PC address

  • Estimated Gas Usage: 8256.

  • Description.

  • The first issue is an External Call To User-Supplied Address vulnerability. It warns us specifically about the withdrawToken(address) that allows users to explicitly supply an address to which the contract makes a call. Such type of invocation is external in context and can leave room for exploitation since we are making a call to an untrusted contract.

A good solution is to disallow an explicit supply of token addresses as input. We can set the token either from the constructor or through a custom function meant for the purpose. If we allow the user to input a token address to withdraw from, users with bad motives may supply an address containing code that may create an unpleasant story. This can cause unexpected behavior.

  • The second issue was tagged State access after the external call for the same function - WithdrawToken. The severity is ‘medium’.
  function withdrawToken(address _token) public {
    require(_token != address(0), "Token is zero address");
    uint tBalance = tokenBalances[msg.sender];
    IERC20(_token).transfer(msg.sender, tBalance);
    tokenBalances[msg.sender] -= tBalance;
  }

In the above code, we modify the state after making an external call. This is not a good practice as it can cause a reentrancy attack if not checkmated.

Fix the issues

Let’s fix the issues and rerun the command. Create a new file WidthrawalFixed.sol, and paste the following code:

 // SPDX-License-Identifier: MIT

  pragma solidity 0.8.18;

  interface IERC20 {
    function transfer(address to, uint256 amount) external returns(bool);
    function allowance(address owner, address to) external returns(uint256);
    function transferFrom(address from, address to, uint amount) external returns(bool);
  }

  contract WithdrawalFixed {
    address public supportedToken;

    mapping (address => uint256) public tokenBalances;
    mapping (address => uint256) public balances;

    constructor (address _supportedToken) {
      supportedToken = _supportedToken;
    }

    function withdrawCelo() public {
      uint _balance = balances[msg.sender];
      if(_balance > 0) {
        balances[msg.sender] -= _balance;
        (bool done,) = address(msg.sender).call{value: _balance}("");
      require(done);
      }
    }

    function depositToken() public {
      uint allowance = IERC20(supportedToken).allowance(msg.sender, address(this));
      require(allowance > 0);
      tokenBalances[msg.sender] += allowance;
      bool done = IERC20(supportedToken).transferFrom(msg.sender, address(this), allowance);
      require(done);
    }

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

    function withdrawToken() public {
      require(supportedToken != address(0), "Token is zero address");
      uint tBalance = tokenBalances[msg.sender];
      require(tBalance > 0, "No balance to withdraw");
      tokenBalances[msg.sender] -= tBalance;
      bool done = IERC20(supportedToken).transfer(msg.sender, tBalance);
      require(done);
    }
  }
  myth analyze contracts/WithdrawalFixed.sol
  ==== External Call To User-Supplied Address ====
  SWC ID: 107
  Severity: Low
  Contract: Withdrawal
  Function name: withdrawCelo()
  PC address: 659
  Estimated Gas Usage: 8260 - 63204
  A call to a user-supplied address is executed.
  An external message call to an address specified by the caller is executed. Note that the callee account might contain arbitrary code and could re-enter any function within this contract. Reentering the contract in an intermediate state may lead to unexpected behavior. Make sure that no state modifications are executed after this call and/or reentrancy guards are in place.

We only got a warning for making a low-level call to the unknown account which might contain arbitrary code. Every other issue is resolved.

Note: Mythil does not guarantee that your contracts are completely free of issues. It may not detect issues relating to business and other logic. If you take a close look at the WithdrawalFixed.sol, you may find out there are logical or known issues not detected. But to a good amount, we are able to avoid common errors that might have gone oversight.

Deployment

If you wish to refresh your memory about deploying to testnet, then this part is for you, else, congratulations on completing the tutorial.

If everything is resolved, we can deploy the contract on the testnet.

  • Override the handhat config file with the code below
import { config as CONFIG } from "dotenv";
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "hardhat-deploy";

CONFIG();

const config: HardhatUserConfig = {
  networks: {
    alfajores: {
      url: process.env.DESTINATION_CHAIN_URI,
      accounts: [`${process.env.PRIVATE_KEY}`],
      chainId: 44787,
    },
  },

  solidity: {
    version: "0.8.18",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      evmVersion: "byzantium",
    },
  },
};

export default config;
  • Install dotenv and hardhat-deploy.
  yarn add --dev dotenv
  yarn add 'hardhat-deploy'
  • Compile the code.
  npx hardhat compile
  • Deploy to Alfajores.
  npx hardhat run scripts/deploy.ts

The complete code for this tutorial can be found on the github.

Conclusion​

In this guide, you learned how to :

  • Install and set up Mythril on window OS.
  • Write simple smart contracts.
  • Analyze smart contracts using Mythril.
  • I explained the analysis report, and we fixed the reported issues.

What next?

To learn how to deploy your dream project on the Celo blockchain, visit the Celo documentation

About the Author​

Isaac Jesse , aka Bobelr is a smart contract/Web3 developer. He has been in the field since 2018, worked as an ambassador with several projects like Algorand and so on as a content producer. He has also contributed to Web3 projects as a developer.

References​

11 Likes

Good job @bobelr

2 Likes

Thank you @OtakIsreal

8 Likes

Welldone!

Hi bro, @bobelr,

Can you please point me to some beginner posts and tutorials? All I see here are mostly intermediate and advance tutorials. Thank you.

2 Likes

@Crux Seeing you’re a new Comer, I’d suggest you get familiar with the Academy. Follow this link to see how to get started Getting Started with Celo Academy

Then go to beginner tag where you’ll find couple of beginner course to help you gain some beginner knowledge you require. For advance and technical tutorials, see Technical Tutorials. Should you need further assistance, do not hesitate to let me know. I’m here to help.

10 Likes
  1. Celo Docs link broken.

  2. Image under installation broken. Kindly fix it

All fixed!

10 Likes