DEV Community

Cover image for Security Best Practices for Smart Contracts in Django Projects
Ebuka-Ukatu
Ebuka-Ukatu

Posted on

Security Best Practices for Smart Contracts in Django Projects

Prerequisites

You should have an intermediate understanding of the following to understand the topic and follow along with the case studies and analysis in this article:

  • You need an understanding of solidity programming language, and if you don’t, preview this tutorial.
  • Intermediate understanding of Python.
  • You need an understanding of Django and how it works with smart contracts.
  • An understanding of Git.
  • An understanding of hardhat.

Introduction

Smart contracts are programmable codes that execute a predicted outcome when the conditions of an agreement are met. Unlike traditional contracts that need the enforcement of third parties, with smart contracts, there's no need.

Django is a web development framework that works with Python, and there are several uses of smart contracts in Django:

  • You can use smart contract to save the data of your Django application on a blockchain.
  • You can use smart contracts for crypto payments on your Django application.
  • Smarts are used in Django to explore and interact with already existing external contracts.

Security is essential for smart contracts. Like any software application deployed for public use, developers consider security. Here are a few crucial benefits of security in smart contracts:

  • Security in smart contracts prevents exploits by malicious actors. A DAO hack is one instance. There are others.
  • Better security means increased trust in smart contracts because there are few incidents where users could lose any asset.

There are best practices developers can use to bolster the security of their smart contracts in Django applications. Using these practices helps developers achieve optimal contract operation.

Smart Contract Security Recommended Practices

Smart contracts are immutable once deployed, and developers must carry out practices that improve smart contracts. Understanding these practices is a prerequisite for smart contract development. Let's delve into them.

A. Use a Linter to Detect Bugs and Vulnerabilities

Linters are programming tools that analyze program source codes and give feedback on bugs and syntax errors. Developers then use this information to better their contracts by fixing the issues before they deploy.

Developers use this information to optimize their smart contract code by fixing the issues before deploying.

There are a few linters for smart contracts written for solidity:

  • Slither: Built using Python. Slither is a security framework that allows you to analyze your contract code, check for errors and vulnerabilities, and then output detailed visual information about these errors if there are any and has an Application Programming Interface (API) that lets anyone write custom analyses.

  • Mythril: Built for EVM chains, this is another security analysis tool for smart contracts. Developers use it to detect common vulnerabilities in contracts like integer overflows. However, it doesn’t apply to the business logic of a function in a contract.

Case Study Using Slither

Here, using a contract example, you will analyze the contract and figure out where you can use the information returned by Slither to optimize your code. Using Remix Integrated Development Environment (IDE), you need to have the following installed:

  • Solidity.
  • Solidity compiler.
  • Solidity compiler version.

You can install each of these independently. However, since you are using Remix for this case study, you don’t. Get into your terminal and write the command and run it:

npm install -g @remix-project/remixd
Enter fullscreen mode Exit fullscreen mode

If you want to have it situated in a directory of your choice, then run the command without -g :

npm install @remix-project/remixd
Enter fullscreen mode Exit fullscreen mode

After you have successfully run the command, you should have this:

Security Best Practices for Smart Contracts in Django Projects

Now that you have it installed, you need to run remixid in your terminal using the command:

remixd
Enter fullscreen mode Exit fullscreen mode

You will have this in your terminal:

Security Best Practices for Smart Contracts in Django Projects

Now that you have done so create a folder for your smart contract, get into your terminal and run the command:

mkdir [new-folder-name]
Enter fullscreen mode Exit fullscreen mode

In your terminal, in place of the new-folder-name, put the name you wish to call your folder.

Afterward, head over to Remix on the homepage, and follow the steps to access the folder:

  • Click on the area shown and select Connect to Localhost:

Security Best Practices for Smart Contracts in Django Projects

  • After the folder loads create a new file for your smart contract.
  • After you have input this solidity contract code in, save, and compile it:
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.4;


// ERC20 token interface is implemented only partially.
// hence some functions are left undefined:
//  - transfer, transferFrom,
//  - approve, allowance.

contract PresaleToken {

    /// @dev Constructor
    /// @param _tokenManager Token manager address.
    function PresaleToken(address _tokenManager, address _escrow) {
        tokenManager = _tokenManager;
        escrow = _escrow;
    }


    /*/
     *  Constants
    /*/

    string public constant name = "TEST Presale Token";
    string public constant symbol = "TST";
    uint   public constant decimals = 18;

    uint public constant PRICE = 606; // 606 SPT per Ether

    //  price
    // Cup is 10 000 ETH
    // 1 eth = 606 presale tokens
    // ETH price ~50$ for 28.03.2017
    // Cup in $ is ~ 500 000$

    uint public constant TOKEN_SUPPLY_LIMIT = 606 * 10000 * (1 ether / 1 wei);



    /*/
     *  Token state
    /*/

    enum Phase {
        Created,
        Running,
        Paused,
        Migrating,
        Migrated
    }

    Phase public currentPhase = Phase.Created;
    uint public totalSupply = 0; // amount of tokens already sold

    // Token manager has exclusive priveleges to call administrative
    // functions on this contract.
    address public tokenManager;

    // Gathered funds can be withdrawn only to escrow's address.
    address public escrow;

    // Crowdsale manager has exclusive priveleges to burn presale tokens.
    address public crowdsaleManager;

    mapping (address => uint256) private balance;


    modifier onlyTokenManager()     { if(msg.sender != tokenManager) throw; _; }
    modifier onlyCrowdsaleManager() { if(msg.sender != crowdsaleManager) throw; _; }


    /*/
     *  Events
    /*/

    event LogBuy(address indexed owner, uint value);
    event LogBurn(address indexed owner, uint value);
    event LogPhaseSwitch(Phase newPhase);


    /*/
     *  Public functions
    /*/

    function() payable {
        buyTokens(msg.sender);
    }

    /// @dev Lets buy you some tokens.
    function buyTokens(address _buyer) public payable {
        // Available only if presale is running.
        if(currentPhase != Phase.Running) throw;

        if(msg.value == 0) throw;
        uint newTokens = msg.value * PRICE;
        if (totalSupply + newTokens > TOKEN_SUPPLY_LIMIT) throw;
        balance[_buyer] += newTokens;
        totalSupply += newTokens;
        LogBuy(_buyer, newTokens);
    }


    /// @dev Returns number of tokens owned by given address.
    /// @param _owner Address of token owner.
    function burnTokens(address _owner) public
        onlyCrowdsaleManager
    {
        // Available only during migration phase
        if(currentPhase != Phase.Migrating) throw;

        uint tokens = balance[_owner];
        if(tokens == 0) throw;
        balance[_owner] = 0;
        totalSupply -= tokens;
        LogBurn(_owner, tokens);

        // Automatically switch phase when migration is done.
        if(totalSupply == 0) {
            currentPhase = Phase.Migrated;
            LogPhaseSwitch(Phase.Migrated);
        }
    }


    /// @dev Returns number of tokens owned by given address.
    /// @param _owner Address of token owner.
    function balanceOf(address _owner) constant returns (uint256) {
        return balance[_owner];
    }


    /*/
     *  Administrative functions
    /*/

    function setPresalePhase(Phase _nextPhase) public
        onlyTokenManager
    {
        bool canSwitchPhase
            =  (currentPhase == Phase.Created && _nextPhase == Phase.Running)
            || (currentPhase == Phase.Running && _nextPhase == Phase.Paused)
                // switch to migration phase only if crowdsale manager is set
            || ((currentPhase == Phase.Running || currentPhase == Phase.Paused)
                && _nextPhase == Phase.Migrating
                && crowdsaleManager != 0x0)
            || (currentPhase == Phase.Paused && _nextPhase == Phase.Running)
                // switch to migrated only if everyting is migrated
            || (currentPhase == Phase.Migrating && _nextPhase == Phase.Migrated
                && totalSupply == 0);

        if(!canSwitchPhase) throw;
        currentPhase = _nextPhase;
        LogPhaseSwitch(_nextPhase);
    }


    function withdrawEther() public
        onlyTokenManager
    {
        // Available at any phase.
        if(this.balance > 0) {
            if(!escrow.send(this.balance)) throw;
        }
    }


    function setCrowdsaleManager(address _mgr) public
        onlyTokenManager
    {
        // You can't change crowdsale contract when migration is in progress.
        if(currentPhase == Phase.Migrating) throw;
        crowdsaleManager = _mgr;
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Now that you complied it, head over to this part of your Remix interface, click on analyze, then the Slither section.

From the errors, you can then work on fixing the smart contract presale code using the error as pointers.

B. Perform Thorough Testing on your Smart Contract

Testing your smart contract helps you identify possible exploitable errors you wouldn't have ordinarily noticed even after going over the code yourself or using any linter.

Given that smart contracts are immutable when deployed to the mainnet. Since they usually deal with massive valued assets, any exploitation would lead to user losses.

Since Linters don't work on the logic of codes, you should use an advanced technique to determine if your code can be deployed and integrated with your Django application. To do this, you could perform the following:

  • Unit testing: Performing thorough unit tests on your smart contract code is necessary because each function gets tested individually to see if the function's logic works. This development process divides all testable parts into units.

Doing this helps you as a developer to know if parts of your
code work as you intend it to and meets the minimum requirement
for security, usability, and reliability. If it doesn't meet these requirements, you can fix the bugs and save those
interacting with it from future exploitation.

Unit tests are an automated type of testing because you use
scripts to carry it out. And to properly carry out a unit test,
you have to consider the following:

  • You should comprehensively understand your smart contract's business logic and workflow.

  • Know the assumptions associated with your smart contract.

  • Check and measure the code coverage of your smart contract.

  • Use a well-developed testing suite like:

    • Foundry
    • Truffle
    • Brownie
    • Remix

Using these suites allows you to perform unit testing faster.

  • Fuzz testing: Your unit tests could be good enough to let you figure out vulnerabilities in your code. However, it's good to run more than one type of test. Fuzz testing enables you to input millions of unexpected data against your smart contract to cause surprising behavioral logic.

It is fast, efficient, and easy to test scenarios or assumptions you might have yet to think about with your smart contract code.

Most programmers use Diligence fuzzing for their smart contracts. It's also good to note that fuzzing used the scribble standard to identify complex vulnerabilities better.

Case Study using Consensys Diligence Fuzzing

Consensys, for a while, had launched the beta version of their diligence fuzzing. However, earlier this year, they launched the tool for use within the developer community.

This case study will use the Consensys diligence fuzzing tool to test a staking smart contract. To run fuzz testing, follow the tutorial:

Step 1: Create an account

You must create a diligence account to carry out the fuzzing test. When you signup, you get 10 hours of free fuzzing time. Use the Google sign-in option to sign up or input your email and create a password:

Security Best Practices for Smart Contracts in Django Projects

Step 2: Get an Application Programming Interface (API) key

Head over to this part of the website:

Security Best Practices for Smart Contracts in Django Projects

Click on Create a new APT Key:

Security Best Practices for Smart Contracts in Django Projects

Give it a name, create, and copy the API:

Security Best Practices for Smart Contracts in Django Projects

Step 3: Create a Working Hardhat working Folder

Here you need to create a working Hardhat project folder. After you have created it, head over to this GitHub repository:

https://github.com/Consensys/scribble-exercise-1/blob/master/contracts/vulnerableERC20.sol

Copy the code into your Hardhat project, and create a new folder for your contracts. Afterward, create a new file for the solidity code, and paste the code.

After you have, compile it by running this command in the project contracts directory on your terminal:

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

Step 4: Install dependencies

Navigate to the root folder you had created for the Hardhat project in the previous step on your terminal and install project-related dependencies by running these commands:

pip3 install diligence-fuzzing
Enter fullscreen mode Exit fullscreen mode

After running the command, next, run this:

npm i -g eth-scribble ganache truffle
Enter fullscreen mode Exit fullscreen mode

And after you do, get on with the next step.

Step 5: Add your Created API Key

Now that you have installed the dependencies, get into the Hardhat root folder you created and create an env where you can add your API key by running this command:

echo FUZZ_API_KEY='your API key here' > .env
Enter fullscreen mode Exit fullscreen mode

In place of your API key here input the key you created initially and run the command.

Step 6: Annotate the Contract

Get into the root directory, and find the contract,
contracts/'the name you saved it with'.sol. After you have, add this to the top of the contract:

/// #invariant "balances are in sync" unchecked_sum(_balances) == _totalSupply;
Enter fullscreen mode Exit fullscreen mode

And above the transfer function, add this:

/// #if_succeeds msg.sender != _to ==> _balances[_to] == old(_balances[_to]) + _value; /// #if_succeeds msg.sender != _to ==> _balances[msg.sender] == old(_balances[msg.sender]) - _value; /// #if_succeeds msg.sender == _to ==> _balances[msg.sender] == old(_balances[_to]); /// #if_succeeds old(_balances[msg.sender]) >= _value;
Enter fullscreen mode Exit fullscreen mode

After you have, run this command:

fuzz arm

Enter fullscreen mode Exit fullscreen mode

And then deploy the contract to your chosen network.

Step 7: Run Fuzz Test

To run the fuzz test from the root folder in your terminal, run the command:

make fuzz

Head over to your dashboard at Diligence and wait a few minutes. The failure or errors in the smart contract will show.

You can further click through the properties to see what’s wrong with the contract and how you could fix its logic.

C. Write Simple Smart Contracts

Smart contracts can be complex depending on what objective you aim for with them. However, simple rules can help you write simple smart contracts.

Simple, smart contracts make your code easier to audit and run tests on, saving you time and making your code more effective. The following are simple rules you can use:

  • Depending on the complexity, break your whole code into more minor contracts focused on a functionality.
  • Apply modular programming principles when writing your contracts to make them reusable across projects.

D. Apply Safe Maths Operations in Contracts

During arithmetic operations, overflow tends to happen, and most times this isn’t the programmer's fault because it is assumed that a data type will hold the data assigned to it. Also, underflows happen, and this is due to the result of an arithmetic calculation being in an absolute value that gets represented.

In solidity, however, unlike most high-level programming languages where an error is shown for an overflow or underflow, this isn’t the case. And since solidity doesn’t show this error, safe maths is used to revert the mathematical operation or transaction when an overflow or underflow happens.

Importing the safe math library when writing a contract helps to validate if a logic will cause an overflow or underflow. However, this library is only available and works for versions of solidity from 0.8 and lower. So, if you use a lower version of solidity when writing your smart contract, importing this operation is a good security measure to prevent the loss of assets or funds.

To import safe maths operations in your solidity code, use this:

Import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.0.0/contracts/math/SafeMath.sol";
Enter fullscreen mode Exit fullscreen mode

E. Use Reentrancy Security Strategies in your Contract

Reentrancy attacks have seen contracts drained of their fund. In solidity, the attack happens when a malicious actor finds an exploitable error in a function. The function getting exploited gives up the control flow of the transaction because the smart contract makes an external call to the contract written by the malicious actor. The unfavorable contract then makes a recursive call to the contract holding the funds, where it then drains it.

Reentrancy attacks are of three types, namely:

  • Single Reentrancy Attack
  • Cross-function Reentrancy Attack
  • Cross-contract Reentrancy Attack

These attacks can happen in different ways. However, a common pseudo-code example is this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


contract PaymentSystem {
    mapping (uint256 => bool) public payments;


    function payUserA(uint256 userId) public {
        if (!payments[userId]) {
            // pay user
            payments[userId] = true;
        }
    }


    function payUserB(uint256 userId) public {
        payments[userId] = true;
        if (!payments[userId]) {
            // pay user
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you check the functions in the code, the first one is exploitable because if a malicious actor is smart enough, they can send multiple transaction payout requests and get paid more money before the mapping gets set to true. However, the second function checks for this, and the first interaction or call gets set to true and subsequent requests won’t pay out.

Using common best security smart contract strategies to prevent reentrancy attacks is the best route to take, namely:

  • Pullpayments
  • ReentrancyGuards
  • Pausable

Using this in your smart contract will help against a reentrancy attack. Pullpayments prevent these attacks by preventing a paying smart contract from interacting directly with a reciever address. Here the malicious actor won’t be able to block execution. To use it in your smart contract, do this:

import "@openzeppelin/contracts/security/PullPayment.sol";
Enter fullscreen mode Exit fullscreen mode

Also, ReentrancyGuards prevent a reentrancy attack on a function, like demonstrated above, to use this in your code:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
Enter fullscreen mode Exit fullscreen mode

Pausable is an emergency stop mechanism you can use in your smart contracts by an authorized account. To use it in your code:

import "@openzeppelin/contracts/security/Pausable.sol";
Enter fullscreen mode Exit fullscreen mode

Perform Audits and Maintenance

Applying security best practices to your smart contract codes is a good thing. However, you will want always to be ahead of any situation should it arise. And for that reason, you should perform regular security audits on your smart contracts because this will help you discover bugs that must have escaped scrutiny.

During security audits, when vulnerabilities are found, you want to carry out a patch or an update quickly before a malicious actor discovers it. Security is always necessary, and you must regularly and constantly carry out audits and maintenance to safeguard against attacks.

Conclusion

Security practices are used to give the best positive outcome possible, increase trust, and prevent exploits of smart contracts. When building your Django applications and integrating smart contracts into them, you wouldn’t want any user to fall prey to security vulnerabilities because smart contracts mainly deal with funds.

We looked at the common types of attacks on smart contracts and how they happen, from arithmetic overflow and underflow to reentrancy attacks. This article's discussions and case studies are enough to set you on a path for applying best security practices in your contracts.

Top comments (0)