How to create Pausable ERC20 Tokens?

Introduction

Tokens are digital currencies in the blockchain world that allow us to conduct numerous online transactions. Consider having a specific type of token that may be stopped as required. This is where pausable ERC-20 tokens come in. They act as a safety switch, preventing tokens from moving for a brief period and making things more secure. When the time is appropriate, the contract owner may turn it back on, and the tokens will begin to move again, exactly like pressing play after a pause. This article will explain why pausable tokens are necessary and how we can create our own pausable ERC20 token using solidity smart contracts.

How to create Pausable ERC20 Token

What are Pausable ERC20 tokens?

A Pausable ERC20 token is a type of digital currency or token that can be deliberately paused or frozen. This new function adds an extra degree of authority and security to our digital tokens. When activated, it temporarily disables the ability to transfer or use the tokens, hence protecting them against any potential risk or problems. We can simply consider it as a quick approach to stop potential problems in their paths, similar to placing a temporary halt to avoid small glitches from growing into severe difficulties.

Benefits of Pausable tokens

The benefits which come along with pausable tokens are as follows -

  1. Increased Security: Pausable ERC-20 tokens act as a security measure that can be swiftly deployed when unusual or suspicious activities are detected. This ability to pause tokens offers a quick response to potential threats, preventing them from spreading or causing harm. It's just like hitting the emergency stop button to prevent minor glitches from converting into major problems.
  2. Compliance & Regulation: When comes to finance and digital assets, regulatory compliance is essential. Pausable ERC-20 tokens give a method for ensuring compliance with regulatory standards. If there's a need to verify a transaction or review compliance issues, pausing the tokens temporarily allows time for these checks without disrupting the overall system.
  3. Improved User Experience: The pausable feature contributes to a positive user experience by addressing issues promptly without causing widespread interruptions.
  4. Flexibility in Token Management: Pausable ERC-20 tokens give token creators and managers a flexible toolset. Whether it's handling unexpected technical glitches, preparing for network upgrades, or managing potential vulnerabilities, the pausable feature offers a level of adaptability that traditional tokens lack.

Creating a Pausable ERC20 token

Pausable functionality is an interesting functionality that a normal ERC20 token can inherit. To make a token pausable token, we need to code two functionalities, pause and unpause, into the smart contract code governing the token. In this tutorial, I will show you how to create a Pausable ERC20 token. Even though we can use the pre-defined template that OpnZeppelin provides, for now, we will be creating the contact and its functionalities from scratch. There are some tools and setups which you will need for this tutorial.

Pre-requisites

  1. Ethereum address and with private key
  2. Node Alchemy or Infura RPC Url
  3. Node.js
  4. Hardhat
  5. Some faucets in your account

You can read my article Smart Contract Deployment Made Easy with Hardhat, where I have defined how we can set up our account and configure the hardhat project in our computer. If you have completed all these prerequisites, then we can directly start with creating a smart contract for the erc20 token.

Create ERC20 Token

In the contracts folder, create a new file named IERC20.sol and write the below code in it.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
/* 
An interface smart contract, contains all the important functions and
events required for creating an ERC20 token according to the standards.
*/
interface IERC20 {
    function name() external view returns(string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address account) external view returns (uint);
    function transfer(address recipient, uint amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint);
    function approve(address spender, uint amount) external returns (bool);
    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);
}

The IERC20.sol is an interface contract that consists of 9 functions and 2 events that are required by any token to be called a valid ERC20 token according to the ERC20 token standard. We will be inheriting this interface contract in our Token contract and define all these methods over there.

Now in the same contracts folder, create a new file named Token.sol. After creating your folder structure should look like this.

Folder Structure

In this token.sol file, we will create a contract Token that will inherit the above-declared interface.

In the Token contract, we will be providing the token metadata i.e., token name and token symbol. We will also use 18 as the token decimal, and we will be defining all the functions declared in the interface IERC20.sol

Let's inherit the interface in our token contract and provide the metadata.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18 ;
/*
Main Contract : Inherits the Interface IERC20.sol
*/
import {IERC20} from "./IERC20.sol";    // importing IERC20.sol
contract Token is IERC20 {
    string private _name = "MyToken";
    string private _symbol = "MTT";
    uint8 private _decimals = 18;
}

Explanation: Here, we have created a contract named Token which implements the IERC20 interface created above. We are also providing the token name as MyToken token symbol as MTT, and the token decimal 18.

Now we will be creating a mapping variable that will help us to keep track of the token and allowance balance of individual addresses on the chain and a variable to store the total supply of our tokens.

uint256 private _totalSupply;

mapping (address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;

Next, we will be creating a constructor using which we will be proving the total supply of our tokens.

constructor(uint256 maxSupply) {
    _totalSupply = maxSupply;
    _balances[msg.sender] = maxSupply;
}

Explanation: In the above code, we are initializing the total supply of the token and allocating all the tokens to the contract owner's address.

The initial setup is done now. We need to write the definition of the functions that were declared in the interface. Let's get started.

/// This funtion returns the token name of the ERC20 token
function name() public view virtual returns (string memory) {
    return _name;
}
/// This function returns the token symbol of the ERC20 token
function symbol() public view virtual returns (string memory) {
    return _symbol;
}
/// This function returns the token decimals of the ERC20 token
function decimals() public view virtual returns (uint8) {
    return _decimals;
}
/// This function returns the total supply or maximum supply of the ERC20 token
function totalSupply() public view virtual returns (uint256) {
    return _totalSupply;
}
/// This function returns the balance of a particular account address
function balanceOf(address account) public view virtual returns (uint256) {
    return _balances[account];
}

Explanation: In the above code, we are defining name, symbol, decimals, and totalSupply functions. These functions return the name, symbol, decimal, and total supply of the token. Apart from these, we are also defining the balanceOf function, which returns the token balance of a particular Ethereum address.

Now we need to define the transfer function, which will be used to transfer tokens from one account address to another

/// This function is used for transferring tokens from one account to another.... calls an internal function _transfer
function transfer(address to, uint256 amount) external virtual returns (bool) {
    address owner = msg.sender;
    if (owner == address(0)) {
        revert("Invalid Sender");
    }
    if (to == address(0)) {
        revert("Invalid Recever");
    }
    updateTokenBalance(owner, to, amount);
    return true;
}

///// function to update token balances
function updateTokenBalance(
    address from,
    address to,
    uint256 amount
) internal virtual {
    uint256 fromBalance = _balances[from];

    if (fromBalance < amount) {
        revert("Insufficient balance");
    }
    unchecked {
        _balances[from] = fromBalance - amount;
    }

    if (to == address(0)) {
        _totalSupply -= amount;
    } else {
        _balances[to] += amount;
    }
    // calling event Transfer
    emit Transfer(from, to, amount);
}

Explanation: In the above code, we are defining two functions. The first transfer function takes in the token amount and address of the receiver as parameters and checks for valid Ethereum addresses. If the address of the owner as well as receiver are valid, then it calls the updateTokenBalance function, which updates the token balances of the accounts.

Next is defining functions that work with the allowance feature of the erc20 token. Allowance means giving rights to an account address to act as a proxy for another account and allowing spending a limited amount of tokens on its behalf.

/// This function returns the allowance amount that an accunt gets from another account
function allowance(
    address owner,
    address spender
) public view virtual returns (uint256) {
    return _allowances[owner][spender];
}

/// This function is used to approve an allowance.... calls an internal function _approve
function approve(
    address spender,
    uint256 amount
) public virtual returns (bool) {
    address owner = msg.sender;
    approveAllowance(owner, spender, amount, true);
    return true;
}

///// internal function to approve allowance
function approveAllowance(
    address owner,
    address spender,
    uint256 amount,
    bool accessEvent
) internal virtual {
    if (owner == address(0)) {
        revert("Invalid Owner");
    }
    if (spender == address(0)) {
        revert("Invalid Spender");
    }
    _allowances[owner][spender] = amount;
    if (accessEvent) {
        emit Approval(owner, spender, amount);
    }
}

Explanation: Here, we are defining three methods. The first one is allowance which returns the allowance value. The second function is approved. This function is used to approve an allowance. This function calls another function named approveAllowance, where the main logic for allowance approval is written. In the approveAllowance function, we check whether the provided addresses are valid or not and then follow up with the approval. After the approval is complete, an event Approval is fired, creating a log of new approvals.

Let's get to the last function transferFrom declared in the interface.

/// This function transfers tokens : used by proxy accounts
function transferFrom(
    address from,
    address to,
    uint256 amount
) external virtual returns (bool) {
    address spender = msg.sender;
    spendAllowanceFromProxy(from, spender, amount);
    updateTokenBalance(from, to, amount);
    return true;
}

///// internal function to deduct allowance or spend allowance
function spendAllowanceFromProxy(
    address owner,
    address spender,
    uint256 amount
) internal virtual {
    uint256 currentAllowance = allowance(owner, spender);
    if (currentAllowance != type(uint256).max) {
        if (currentAllowance < amount) {
            revert("Insufficient allowance");
        }
        approveAllowance(owner, spender, currentAllowance - amount, false);
    }
}

Explanation: The above-defined transferFrom function is used to transfer tokens from an allowance account holder to another account. This function calls two different functions. The first one spendAllowanceFromProxy, calls the internal function approveAllowance to change the state of allowance tokens left with the spender address and updateTokenBalance function, which updates the token balance of an address.

With all these functions, we have successfully created a basic erc20 token contract. We can deploy this contract on any testnet to test the basic functionalities of the contract.

Write Pause functionality contract code

After we have created a basic erc20 contract, it is now time to add features that we want our erc20 token to have. Thus now we will be adding the pause and unpause functionality in our erc20 token and modify the Token.sol i.e., the main token contract according to the added functionality.

To make it easy and understandable, we will be adding the pause functionality using a separate contract file which we will be inheriting in our main Token.sol and modifying it accordingly.

Create a new file named Pausable.sol in your contracts folder and write the below code in it.

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

/*
This contract is used for pausing the transfers of the token.
The functions Pause and unPause can only be accessed by the 
contract owner.
It pauses the token transfer for desired amount of time
*/
abstract contract Pausable {
    address private _owner;
    bool private _paused;

    event Paused(
        address indexed currentOwner,
        uint indexed timeStamp,
        bool indexed paused
    );

    constructor() {
        _owner = msg.sender;
        _paused = false;
    }

    modifier onlyOwner() {
        require(_owner == msg.sender, "Not Owner");
        _;
    }

    // This function returns a boolean value represention token paused or not
    function isPaused() internal view virtual returns (bool) {
        return _paused;
    }

    // This function is used to Pause the token transfers, can be called only by owner
    function Pause() public virtual onlyOwner returns (bool) {
        _paused = true;
        emit Paused(msg.sender, block.timestamp, true);
        return true;
    }

    // This function is used to Remove Pause from the token transfers, can be called only by owner
    function unPause() public virtual onlyOwner returns (bool) {
        _paused = false;
        emit Paused(msg.sender, block.timestamp, false);
        return true;
    }
}

Explanation: Here, we are creating an abstract contract Pausable where we are declaring the state of the contract i.e., whether the token activities are paused or not, using the variable _paused. This contract has three functions Pause, unPause, and isPaused. The first two functions change the state of the contract by pausing and unpausing the activities, and the third function isPaused, returns the state of the contract, whether it is paused or not.

Let's modify our Token.sol file to add the Pausable feature in our token.

First, we need to import the Pausable contract and inherit it in our token contract

import {IERC20} from "./IERC20.sol";    // importing IERC20.sol
import {Pausable} from "./Pausable.sol";
contract Token is IERC20,Pausable {

Now we need to add a check before transfer functions and other token functions that we want to stop during the pause. For example, let's stop the token transfer during the pause. To do see, we need to add a check

require(isPaused() == false, "Transfer Paused");

inside the transfer and transferFrom function at the beginning. This line will check whether the token transfer is paused or not. If true, then it will revert the transaction providing a message "Transfer Paused".

After inserting the check into one of the transfer functions, the code will look like this.

function transfer(address to, uint256 amount) external virtual returns (bool) {
    require(isPaused() == false, "Transfer Paused");
    address owner = msg.sender;
    if (owner == address(0)) {
        revert("Invalid Sender");
    }
    if (to == address(0)) {
        revert("Invalid Recever");
    }
    updateTokenBalance(owner, to, amount);
    return true;
}

Similarly, add the check to the transferFrom function.

function transferFrom(
    address from,
    address to,
    uint256 amount
) external virtual returns (bool) {
    require(isPaused() == false, "Transfer Paused");
    address spender = msg.sender;
    spendAllowanceFromProxy(from, spender, amount);
    updateTokenBalance(from, to, amount);
    return true;
}

Congratulations, now your smart contract is ready for deployment. You have three contract files, IERC20.sol, Token.sol, and Pausable.sol. We just need to deploy them. To deploy contracts using hardhat, you can read my article Smart Contract Deployment Made Easy with Hardhat.

After deploying the contract, you can use the pause and unpause feature by calling those respective functions.

Conclusion

Pausable ERC-20 tokens emerge as a vital asset in blockchain's dynamic landscape. Their ability to temporarily halt transactions ensures security, compliance, and user confidence. By embracing Solidity smart contracts, developers can seamlessly integrate pause and unpause functionalities, adding layers of protection to digital assets.

FAQs

Q. What situations might prompt the use of pausable ERC-20 tokens?

A. Pausable ERC-20 tokens are particularly useful in scenarios where immediate action is needed to prevent potential risks or threats. For instance, if unusual activities or security breaches are detected, the contract owner can swiftly pause token transfers to mitigate any potential damage. This mechanism acts as an emergency brake, halting token movements until the situation is assessed and resolved.

Q. How do pausable ERC-20 tokens impact user experience?

A. Pausable tokens enhance user experience by addressing issues without causing widespread disruptions. If a technical glitch, vulnerability, or regulatory review arises, the contract owner can temporarily pause token transfers. This approach prevents users from unknowingly engaging in transactions during uncertain times. As a result, users can transact with confidence, knowing that their assets are protected and that the platform is responsive to potential challenges.


Similar Articles