How To Create An ERC-20 Token Sale With Solidity And Deploy On Ethereum Part II

How To Create An ERC-20 Token Sale With Solidity And Deploy On Ethereum – Part II

In this blog post, we will continue on the previous post and learn How to Create a custom ERC-20 token on the Ethereum Blockchain using Solidity and deploy it to a public test net.

In this section, we will learn how to build a token crowd sale smart contract for our ERC20 token NZT. This is the contract that will facilitate end users to interact with our ERC20 Smart Contract. In the process, we will do a few things:

  • Allocate funds to our crowdsale smart contract from NZT’s total supply
  • Set the price of our token in wei
  • Assign a crowd sale administrator
  • Buy Tokens
  • End the crowd sale

This is part of a three part series tutorial.

Part I – Create a Custom ERC20 Token With Solidity

Part II – Create a Crowd sale Token Contract With Solidity

Part III – Create a Frontend to interact with our Smart Contracts

You can find the complete source code of both smart contracts on the github page

If you are excited about this, then let’s get it!

Building a Token Crowd sale Smart Contract for an ERC20 Token With Solidity

Declaring The Crowd Sale Smart Contract And Test Script

The first thing we will do is create a new contract ./contracts/NzouaTokenSale.sol and inside the file. After that, we will also create a new test script called ./test/NzouaTokenSale.test.js to implement our test scripts.

// Create a new smart contract file inside the ./contract/ sub-directory
ERC20-Token $ touch ./contracts/NzouaTokenSale.sol

// Create a new test script file inside the ./test/ sub-directory
ERC20-Token $ touch ./test/NzouaTokenSale.test.js

The first thing we will do is sketch out our Crowd Sale Smart contract. Inside ./contracts/NzouaTokenSale.sol let’s write the following code:

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    constructor() {

        // Assign an admin, which is an external address that will have special
        // priviledge other accounts won't have (e.g. End the token sale)


        // Assign Token Contract to the crowd sale

        // Token Price: How much Eth will it costs to sell our token

    }
}

Inside our constructor() function, we will do a couple of things:

  • Assign an admin which is an external address that will have special priviledges over other accounts (e.g. End the crowd sale)
  • Assign the ERC20 Token Smart Contract to the crowd sale smart contract for management
  • Set the price of NZTs. We will have to determine how much ETH it will cost to sell a token.

Now, we are going to create a new migration for our crowd sale smart contract. Go inside ./migrations/2_deploy_contracts.js, and update the migration file like so:

const NzouaToken = artifacts.require("NzouaToken");
const NzouaTokenSale = artifacts.require("NzouaTokenSale");

module.exports = async function (deployer, network, accounts) {
    await deployer.deploy(NzouaToken, 1000000); // Pass initial supply as argument of the deployer.
    await deployer.deploy(NzouaTokenSale)
};

We will come back to the migrations file and extend it later. But for now, let’s go to ./test/NzouaTokenSale.test.js and start writing our tests

var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {
    let tokenSale;

    describe('Contract Attributes', async () => {
        it('Initializes the contract with the correct values', async () => {
            tokenSale = await NzouaTokenSale.deployed();
            assert.notEqual(tokenSale.address, 0x0, 'The smart contract address was set')
        })
    })

});

In this test script, we ensure that our deployed version of the crowd sale smart contract has a valid address that is not 0x0. Our test should pass successfully

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaToken
    ✓ Initializes the contract with the appropriate attributes (114ms)
    ✓ Sets the total supply on deployment (65ms)
    ✓ Allocates the total supply to Contract Owner (61ms)
    ✓ Transfers tokens (4611ms)
    ✓ Approves tokens for delegated tranfers (310ms)
    ✓ Handles delegated NZT tranfers (1223ms)

  Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values (65ms)


  7 passing (7s)

We are now going to assign an admin that will have special priviledge on the crowd sale smart contract.

Assigning admin priviledge to an account

Inside ./contracts/NzouaTokenSale.sol let’s declare the admin state variable and initialize it inside the constructor like so:

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    // Declare an admin variable which will have super priviledge over the smart contract
    address admin;

    constructor() {

        // Assign an admin, which is an external address that will have special
        // priviledge other accounts won't have (e.g. End the token sale)
        admin = msg.sender;


        // Assign Token Contract to the crowd sale

        // Token Price: How much Eth will it costs to sell our token

    }
}

We will write a test script later on to make sure the right address is set as the admin. For now now, let’s move on the the next setp, which is assigning the ERC20-token contract to the crowd sale contract.

Assign ERC20 Token Contract To The Crowd Sale Smart Contract

We have to assign the ERC20 tokens to the crowd sale contract, so users can actually buy them. Under the hood, the crowd sale contract will execute the transfer() function inside the ERC20 contract to trigger the buy and transfer of tokens between accounts. To do that, we will add a reference to the ERC20 token contract inside the crowd sale contract via the crowd sale contract’s constructor.

First, let’s go to ./test/NzouaTokenSale.test.js and write some test scripts for this. The goal would be to ensure that a reference of the NZT token exists inside the crowd sale contract:

var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {
    let tokenSale;

    describe('Contract Attributes', async () => {
        it('Initializes the contract with the correct values', async () => {
            tokenSale = await NzouaTokenSale.deployed();
            assert.notEqual(tokenSale.address, 0x0, 'The smart contract address was set')
        });

        it('References the ERC20 Token Contract', async () => {
            tokenSale = await NzouaTokenSale.deployed();
            assert.notEqual(await tokenSale.tokenContract(), 0x0, 'The token contract is referenced')
        });
    })

});

This test should fail because we have not yet set the reference to the ERC20 token contract inside the constructor of the crowd sale contract:

ERC20-Token $ truffle test --network ganache

// output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      1) References the ERC20 Token Contract
    > No events were emitted


  7 passing (6s)
  1 failing

  1) Contract: NzouaTokenSale
       Contract Attributes
         Initializes the contract with the correct values:
     TypeError: tokenSale.tokenContract is not a function

[... omitted output ...]

To make this pass, let’s go to ./contracts/NzouaTokenSale.sol and update our contract, by:

  • Importing the ERC20 Token contract inside the crowd sale contract.
  • Declaring a tokenContract variable of type NzouatToken.
  • Initializing the token instance inside the constructor of the crowd sale.
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    // Declare an admin variable which will have super priviledge over the smart contract
    address admin;

    // Declaring the ERC20 token contract variable
    NzouaToken public tokenContract;

    constructor(NzouaToken _tokenContract) {

        // Assign an admin, which is an external address that will have special
        // priviledge other accounts won't have (e.g. End the token sale)
        admin = msg.sender;


        // Assign Token Contract to the crowd sale
        tokenContract = _tokenContract;

        // Token Price: How much Eth will it costs to sell our token

    }
}

If we do not pass the token contract as an argument, the test will fail with an error similar to this:

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Error: while migrating NzouaTokenSale: Invalid number of parameters for "undefined". Got 0 expected 1!

[... omitted output ...]

To do to make our contract pass, is to pass the ERC20 token address as an argument to the crowd sale deployer when it gets deployed, so it can reference it before deployment.

const NzouaToken = artifacts.require("NzouaToken");
const NzouaTokenSale = artifacts.require("NzouaTokenSale");

module.exports = async function (deployer, network, accounts) {
    // Pass initial supply as argument of the deployer.
    await deployer.deploy(NzouaToken, 1000000); 

    // pass the ERC20 Token contract address to the crowd sale deployer
    await deployer.deploy(NzouaTokenSale, NzouaToken.address)
};

Now, our test will pass as it should:

ERC20-Token $ truffle test --network ganache

// output on the console

[... omitted output ...]

  Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (88ms)


  8 passing (11s)

Now the next thing we will do is set the price of our NZT tokens in wei, which is the smallest unit of measurement of ETH.

Set Token Price (in Wei)

The first thing we will do is write a test script that ensures that the token price is set correctly. We will set our tokenPrice at 1000000000000000 wei. If we go to https://eth-converter.com/ and paste that value inside the wei input field, it will return something like this:

As we can see, 1000000000000000 wei = 0.001 ETH. This means that 1 NZT = 0.001 ETH. But we will set the value in wei, because Solidity is not very good at dealing with float values.

. Inside ./test/NzouaTokenSale.test.js let’s delcare the variable tokenPrice and initialize it to 1000000000000000 and then write our test.

var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {
    let tokenSale;
    let tokenPrice = 1000000000000000; // in wei

    describe('Contract Attributes', async () => {
        
        [... omitted code ...]

        it('References the ERC20 Token Contract', async () => {
            tokenSale = await NzouaTokenSale.deployed();
            assert.notEqual(await tokenSale.tokenContract(), 0x0, 'The token contract is referenced')
        });
        it('Sets the price of ERC20 Token Correctly', async () => {
            tokenSale = await NzouaTokenSale.deployed();
            let price = await tokenSale.tokenPrice();
            assert.qual(price.toNumber(), tokenPrice, 'The token price is set correctly')
        });
    })

});

The test is supposed to fail because it does not know about the tokenPrice() funtion yet.

ERC20-Token $ truffle test --network ganache

// output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (51ms)
      1) Sets the price of ERC20 Token Correctly
    > No events were emitted


  8 passing (6s)
  1 failing

  1) Contract: NzouaTokenSale
       Contract Attributes
         Sets the price of ERC20 Token Correctly:
     TypeError: tokenSale.tokenPrice is not a function

[... omitted output ...]

We will now go inside ./contracts/NzouaTokenSale.sol and update our smart contract like so:

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    // Declare an admin variable which will have super priviledge over the smart contract
    address admin;

    // Declaring the ERC20 token contract variable
    NzouaToken public tokenContract;

    // Declare tokenPrice state variable
    uint256 public tokenPrice;

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        // Assign an admin, which is an external address that will have special
        // priviledge other accounts won't have (e.g. End the token sale)
        admin = msg.sender;


        // Assign Token Contract to the crowd sale
        tokenContract = _tokenContract;

        // Token Price: How much Eth will it costs to sell our token
        tokenPrice = _tokenPrice;
    }
}

If we run the test, it will fail because the crowd sale migration function is expecting another argument, which is the tokenPrice (as seen inside the constructor). The error will look like this:

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Error: while migrating NzouaTokenSale: Invalid number of parameters for "undefined". Got 0 expected 1!

[... omitted output ...]

To make this test finally pass, let’s go back to ./migrations/NzouaTokenSale.js and initialize the tokenPrice variable inside the crowd sale deployer, like so:

const NzouaToken = artifacts.require("NzouaToken");
const NzouaTokenSale = artifacts.require("NzouaTokenSale");

module.exports = async function (deployer, network, accounts) {
    // Pass initial supply as argument of the deployer.
    await deployer.deploy(NzouaToken, 1000000); 

    // pass the ERC20 Token contract address to the crowd sale deployer, along with the token price
    await deployer.deploy(NzouaTokenSale, NzouaToken.address, 1000000000000000)
};

The tes will now pass

ERC20-Token $ truffle test --network ganache

// output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (60ms)
      ✓ Sets the price of ERC20 Token Correctly (39ms)


  9 passing (3s)

In this section, we learned how to assign a crowd sale administrator who can perform tasks such as end a crowd sale (which we will build in a later section). Next, we explore how to set NZT’s token price in wei, which is the smallest unit of measurement on the Ethereum blockchain. In the next section, we will learn how to implement functionalities that allow users to buy NZT tokens via the crowd sale smart contract. Let’s do it.

Buying Tokens

in this section of the tutorial, we are going to expand on our crowd sale smart contract and implement the following:

  • Buying tokens
  • Allocating NZT tokens from the ERC20 token contract to the crowd sale contract

buyTokens() function

To accomplish this, we are going to do the following:

  • Keep track of token sold
  • Trigger a Sell Event
  • Require that value is equal to tokens
  • Require that contract has enough tokens
  • Require that a transfer is successful

First, let’s go to ./contracts/NzouaTokenSale.sol and declare our buyTokens() like so:

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable {
        
    }

}

It is important to notice that the function buyTokens() is declared as payable which means that we want users to send ETH using this function. We will come back to it later. Next let’s write some tests.

Inside ./test/NzouaTokenSale.test.js we will write an easy test that keeps track of how many tokens were sold. We will declare two variables we would like to track of: adminAccount and buyerAccount. After that we will write a ensures that the amount of tokens sold is incremented properly.

var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    let tokenSale;
    let tokenPrice = 1000000000000000; // in wei
    let adminAccount = accounts[0];
    let buyerAccount = accounts[1];

    describe('Contract Attributes', async () => {
        
        [... omitted code ...]
    });

    describe('Facilitates Token Buying', async () => {
        it('Keeps track of token sold', async () => {
            tokenSale = await NzouaTokenSale.deployed();
            const numberOfTokens = 10;
            let valueOfTokens = numberOfTokens * tokenPrice;
            
            try {
                let receipt = await tokenSale.buyTokens(numberOfTokens, {
                    from: buyerAccount,
                    value: valueOfTokens
                });
                let amountSold = await tokenSale.tokensSold();
                assert.equal(amountSold.toNumber(), numberOfTokens, 'increments the number of token sold')

            } catch (error) {
                assert(error.message.indexOf('revert') >= 0);
            }
        });

    })


});

In the test above, the goal is to buy 10 NZT tokens and ensure that the number of tokens sold is incremented properly. To to that, we needed to calculate the valueOfTokens the buyerAccount is about to buy like so:

let valueOfTokens = numberOfTokens * tokenPrice;

We then passed that value to the buyTokens() function.

If we run the test, it will fail because the tokenSold state variable has not been implemented yet inside the buyTokens() function.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (45ms)
      ✓ Sets the price of ERC20 Token Correctly (51ms)
    Facilitates Token Buying
      1) Keeps track of token sold
    > No events were emitted


  9 passing (5s)
  1 failing

  1) Contract: NzouaTokenSale
       Facilitates Token Buying
         Keeps track of token sold:

      increments the number of token sold
      + expected - actual
  
      + 10       - 0

[... omitted output ...]

To make the test pass, let’s go back to ./contracts/NzouaTokenSale.sol, declare the state variable tokensSold and increment its value every time a user buys tokens.

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    // Declaring a tokensSold variable
    uint256 public tokensSold;

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{
        
       // Keep track of tokensSold
        tokensSold += _numberOfTokens;

    }

}

Our test should pass successfully now.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (75ms)
      ✓ Sets the price of ERC20 Token Correctly (49ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (139ms)


  10 passing (4s)

Triggering a Sell() Event

First, let’s go ./test/NzouaTokenSale.test.js and we will ensure that

  • The transaction receipt contains the logs[] array with at least one event,
  • The event is a Sell() event,
  • The buyerAccount is verified
  • The number of tokens sold is adjusted
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

    describe('Facilitates Token Buying', async () => {
        it('Keeps track of token sold', async () => {
            
            [... omitted code ...]
        });

        it('Triggers the Sell() Event', async () => {
            tokenSale = await NzouaTokenSale.deployed();
            const numberOfTokens = 10;
            let valueOfTokens = numberOfTokens * tokenPrice;

            try {
                let receipt = await tokenSale.buyTokens(numberOfTokens, {
                    from: buyerAccount,
                    value: valueOfTokens
                });

                // Verify transaction logs for Events
                assert.equal(receipt.logs.length, 1, 'triggers one event');
                assert.equal(receipt.logs[0].event, 'Sell', 'should be the "Sell()" event');
                assert.equal(receipt.logs[0].args._buyer, buyerAccount, 'logs the account that purchased the tokens');
                assert.equal(receipt.logs[0].args._amount, numberOfTokens, 'logs the number of tokens purchased');
            } catch (error) {
                assert(error.message.indexOf('revert') >= 0);
            }

        });
    })


});

Our test will fail because the buyTokens() function is expencted to emit/trigger a Sell() event.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (52ms)
      ✓ Sets the price of ERC20 Token Correctly (40ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (205ms)
      1) Triggers the Sell() Event
    > No events were emitted


  10 passing (4s)
  1 failing

1) Contract: NzouaTokenSale
       Facilitates Token Buying
         Triggers the Sell() Event:

      triggers one event
      + expected - actual
  
      + 1        - 0

[... omitted output ...]

To fix this, let’s go back to ./contracts/NzouaTokenSale.sol and do the following:

  • Declare a Sell() Event
  • Trigger/Emit the Sell() event inside the buyTokens() function
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    // Declaring a tokensSold variable
    uint256 public tokensSold;

    // Declaring the Sell() Event
    event Sell(address _buyer, uint256 _amount);

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{
        
       // Keep track of tokensSold
        tokensSold += _numberOfTokens;

           [... omitted code ...]

        // Emit/Trigger the Sell() event
        emit Sell(msg.sender, _numberOfTokens);

    }

}

Our test will now pass successfully

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (75ms)
      ✓ Sets the price of ERC20 Token Correctly (49ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (178ms)
      ✓ Triggers the Sell() Event (124ms)


  11 passing (4s)

Require that value is equal to tokens

In this section will test to see if we can buy tokens that are different from the value we are sending (we try and buy 10 NZT tokens for 1 wei). So let’s open ./test/NzouaTokenSale.test.js and add the following test:

var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

    describe('Facilitates Token Buying', async () => {
        it('Keeps track of token sold', async () => {
            
            [... omitted code ...]
        });

        it('Triggers the Sell() Event', async () => {
            
            [... omitted code ...]
        });

        it('Requires that value is equal to tokens to buy', async () => {
            tokenSale = await NzouaTokenSale.deployed();
            const numberOfTokens = 10;
            let valueOfTokens = numberOfTokens * tokenPrice;


            try {
                let receipt = await tokenSale.buyTokens(numberOfTokens, {
                    from: buyerAccount,
                    value: 1 // Trying to buy 10 NZT tokens for 1 Wei
                });
                assert.noEqual(receipt, true, 'Buyer CAN underpay or overpay 10 NZTs for 1 Wei');
            } catch (error) {
                assert(error.message.indexOf('revert') >= 0, 'msg.value should equal the number of tokens in wei');
            }

        });
    })


});

This test will not pass and we will catch the assertion error inside the catch(){} statement.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (58ms)
      ✓ Sets the price of ERC20 Token Correctly (49ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (169ms)
      ✓ Triggers the Sell() Event (206ms)
      1) Requires that value is equal to tokens to buy

    Events emitted during test:
    ---------------------------

    NzouaTokenSale.Sell(
      _buyer: 0x838235F38b782Ce6d40e04469767842D91DfA162 (type: address),
      _amount: 10 (type: uint256)
    )


    ---------------------------


  11 passing (5s)
  1 failing

  1) Contract: NzouaTokenSale
       Facilitates Token Buying
         Requires that value is equal to tokens to buy:
     AssertionError: msg.value should equal the number of tokens in wei

[... omitted output ...]

To make our test pass, we will need to write a function that ensures that this operation let valueOfTokens = numberOfTokens * tokenPrice; happens without the risk of causing variable/operation overflows. let’s go to ./contracts/NzouaTokenSale.sol and do the following:

  • Write a multiply() function that ensures that valueOfTokens variable does not overflow.
  • Ensure that the value (msg.value) being sent by the buyer) is equal to the amount of tokens to buy with a require() statement.
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{

        // Require that the msg.value sent by the buyer 
        // is equal to the amount of NZT token they want to buy
        require(msg.value == multiply(_numberOfTokens, tokenPrice));
        
        // Keep track of tokensSold
        tokensSold += _numberOfTokens;

        // Emit/Trigger the Sell() event
        emit Sell(msg.sender, _numberOfTokens);

    }

    // This function will ensure a safe mathematical operation 
    // And prevent variable overflow
    function multiply(uint x, uint y) internal pure returns(uint z){
        require(y == 0 || (z = x * y) / y == x);
    }

}

Now our test will pass as expected

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (58ms)
      ✓ Sets the price of ERC20 Token Correctly (53ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (190ms)
      ✓ Triggers the Sell() Event (187ms)
      ✓ Requires that value is equal to tokens to buy (133ms)


  12 passing (6s)

Ensures Contract has enough tokens

Let’s go inside ./test/NzouaTokenSale.test.js and write some tests.

  • First, we will import the ERC20 token contract inside the the test file
  • Then we will grab a deployed instance of the token inside the test
  • Next we will use the transfer() of the token instance to allocate 75% (750000 NZT) of the token supply to the crowdsale contract
  • Finally, we will try to buy 800000 NZT tokens using the buyerAccount. this transaction should fail, because the crowd sale contract balance is 750000 NZT; hence will be short 50000 NZT
var NzouaToken = artifacts.require('./NzouaToken');
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

    describe('Facilitates Token Buying', async () => {

        [... omitted code ...]

        it('Ensures contract has enough tokens', async () => {
            token = await NzouaToken.deployed();
            tokenSale = await NzouaTokenSale.deployed();

            const numberOfTokens = 10;
            let valueOfTokens = numberOfTokens * tokenPrice;

            // 75% of the total supply 
            // will be llocated to the token sale
            const tokensAvailable = 750000;
            let receipt = await token.transfer(tokenSale.address, tokensAvailable, {
                from: adminAccount
            })

            // Allocate funds to the Crowd Sale Contract
            let tokenSaleBalance = await token.balanceOf(tokenSale.address);
            assert.equal(tokenSaleBalance.toNumber(), tokensAvailable, 'Contract received funds')

            // Trying to buy 800000 NZT tokens 
            // Which is more than the available balance of 750000 NZT

            let failedReceipt = await tokenSale.buyTokens.call(800000, {
                from: buyerAccount,
                value: valueOfTokens
            });
            assert.notEqual(failedReceipt, true, 'Buyer CAN buy more token than available balance');

        });
    })


});

If we run our test it will fail and revert because this buyerAccount cannot buy more token than what is available.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (57ms)
      ✓ Sets the price of ERC20 Token Correctly (71ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (129ms)
      ✓ Triggers the Sell() Event (163ms)
      ✓ Requires that msg.value is equal to tokens to buy (121ms)
      1) Ensures contract has enough tokens

    Events emitted during test:
    ---------------------------

    [... omitted output ...]

    ---------------------------


  12 passing (4s)
  1 failing

  1) Contract: NzouaTokenSale
       Facilitates Token Buying
         Ensures contract has enough tokens:
     Error: Returned error: VM Exception while processing transaction: revert

[... omitted output ...]

To make it pass, We will do two things:

  • Add a require() statement inside the buyTokens() function to ensure that the crowd sale contract has enough tokens for buys
  • We will wrap our test inside a try{}catch{} statement to make sure we catch errors while trying to send more funds that what is available

Go to ./contracts/NzouaTokenSale.sol and add the require() statement like so

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{

        // Require that the msg.value sent by the buyer 
        // is equal to the amount of NZT token they want to buy
        require(msg.value == multiply(_numberOfTokens, tokenPrice));

        // Require crowd sale contract to have enough token in balance
        require(tokenContract.balanceOf(address(this)) >= _numberOfTokens);
        
        // Keep track of tokensSold
        tokensSold += _numberOfTokens;

        // Emit/Trigger the Sell() event
        emit Sell(msg.sender, _numberOfTokens);

    }

    // This function will ensure a safe mathematical operation 
    // And prevent variable overflow
    function multiply(uint x, uint y) internal pure returns(uint z){
        require(y == 0 || (z = x * y) / y == x);
    }

}

Now, go to ./test/NzouaTokenSale.test.js and add update our test like so:

var NzouaToken = artifacts.require('./NzouaToken');
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

    describe('Facilitates Token Buying', async () => {

        [... omitted code ...]

        it('Ensures contract has enough tokens', async () => {
            token = await NzouaToken.deployed();
            tokenSale = await NzouaTokenSale.deployed();

            const numberOfTokens = 10;
            let valueOfTokens = numberOfTokens * tokenPrice;

            // 75% of the total supply 
            // will be llocated to the token sale
            const tokensAvailable = 750000;
            let receipt = await token.transfer(tokenSale.address, tokensAvailable, {
                from: adminAccount
            })

            // Allocate funds to the Crowd Sale Contract
            let tokenSaleBalance = await token.balanceOf(tokenSale.address);
            assert.equal(tokenSaleBalance.toNumber(), tokensAvailable, 'Contract received funds')

            // Trying to buy 800000 NZT tokens 
            // Which is more than the available balance of 750000 NZT
            try {
                let failedReceipt = await tokenSale.buyTokens.call(800000, {
                    from: buyerAccount,
                    value: numberOfTokens * tokenPrice
                });
                assert.equal(failedReceipt, true, 'Buyer CAN buy more token than the available balance');
            } catch (error) {
                // console.log(error.message)
                assert(error.message.indexOf('revert') >= 0, 'Buyer cannot buy more than available tokens');
            }

        });
    })


});

Our test will now pass successfully

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (46ms)
      ✓ Sets the price of ERC20 Token Correctly (53ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (119ms)
      ✓ Triggers the Sell() Event (233ms)
      ✓ Requires that msg.value is equal to tokens to buy (48ms)
      ✓ Ensures contract has enough tokens (331ms)


  13 passing (4s)

The last thing we need to do in this section is ensure that the transfer after the buy was successful.

Require a successful transfer of tokens

Let’s write a test first. We ensure that the balance of the buyerAccount increased and the balance of the tokenSale contract decreased by the same account.

var NzouaToken = artifacts.require('./NzouaToken');
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

    describe('Facilitates Token Buying', async () => {

        [... omitted code ...]

        it('Ensures contract has enough tokens', async () => {
            
            [... omitted code ...]

        });

        it('Required a successful Transfer of Tokens', async () => {
            token = await NzouaToken.deployed();
            tokenSale = await NzouaTokenSale.deployed();

            const numberOfTokens = 10;


            const tokensAvailable = 750000;
            await token.transfer(tokenSale.address, tokensAvailable, {
                from: adminAccount
            })

            let tokenSaleBalance = await token.balanceOf(tokenSale.address);
            assert.equal(tokenSaleBalance.toNumber(), tokensAvailable, 'Contract received funds successfully.')

            await tokenSale.buyTokens.call(numberOfTokens, {
                from: buyerAccount,
                value: numberOfTokens * tokenPrice
            });

            // Grab the new balance of the buyerAccount
            buyerBalance = await token.balanceOf(buyerAccount);
            buyerBalance = buyerBalance.toNumber()
            assert.equal(buyerBalance, numberOfTokens, 'Buyer Balance updated');

            // Grab the new balance of the Token Sale Contract
            tokenSaleBalance = await token.balanceOf(tokenSale.address);
            tokenSaleBalance = tokenSaleBalance.toNumber()
            assert.equal(tokenSaleBalance, tokensAvailable - numberOfTokens, 'Token Sale Balance updated');

        });

    })

});

If we run this tes it will fail because we are trying to purchase more tokens that what is available for buy.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (49ms)
      ✓ Sets the price of ERC20 Token Correctly (42ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (196ms)
      ✓ Triggers the Sell() Event (226ms)
      ✓ Requires that msg.value is equal to tokens to buy (60ms)
      ✓ Ensures contract has enough tokens (333ms)
      1) Required a successful Transfer of Tokens
    > No events were emitted


  13 passing (5s)
  1 failing

  1) Contract: NzouaTokenSale
       Facilitates Token Buying
         Required a successful Transfer of Tokens:
     Error: Returned error: VM Exception while processing transaction: revert The account has low funds -- Reason given: The account has low funds.

[... omitted output ...]

To make this pass, let’s go to ./contracts/NzouaTokenSale.sol and require a successful transfer, before updating the amount of tokens sold.

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{

        // Require that the msg.value sent by the buyer 
        // is equal to the amount of NZT token they want to buy
        require(msg.value == multiply(_numberOfTokens, tokenPrice));

        // Require crowd sale contract to have enough token in balance
        require(tokenContract.balanceOf(address(this)) >= _numberOfTokens);

        // Require a successful transfer of tokens 
        require(tokenContract.transfer(msg.sender, _numberOfTokens));
        
        // Keep track of tokensSold
        tokensSold += _numberOfTokens;

        // Emit/Trigger the Sell() event
        emit Sell(msg.sender, _numberOfTokens);

    }

    // This function will ensure a safe mathematical operation 
    // And prevent variable overflow
    function multiply(uint x, uint y) internal pure returns(uint z){
        require(y == 0 || (z = x * y) / y == x);
    }

}

Depending on your setup, this test might still fail. That was my case and I spent hours trying to debug this one. Because I previously incorporated this test inside the contract(‘NzouaTokenSale’, …) as a describe(‘Required a successful Transfer of Tokens’, …) function, it did not work for some reasons, and my guess is that the resource was being utilized by another test, while this one was trying to run.

To make this work with the current setup, we will declare a separate contract() test. The new contract() test will be called NzouaTokenSale – Successful Transfer and will look like this:

var NzouaToken = artifacts.require('./NzouaToken');
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

});

contract('NzouaTokenSale - Successful Transfer', async (accounts) => {
    let tokenSale;
    let token;
    let tokenPrice = 1000000000000000; // in wei
    const adminAccount = accounts[0];
    const buyerAccount = accounts[1];

    beforeEach("setup all contracts", async () => {
        token = await NzouaToken.deployed();
        tokenSale = await NzouaTokenSale.deployed();
    });

    it('Required a successful Transfer of Tokens', async () => {
        let tokenSaleBalance;
        let buyerBalance;
        // adminAccount = await tokenSale.getAdmin();

        const numberOfTokens = 10;


        const tokensAvailable = 750000;

        await token.transfer(tokenSale.address, (tokensAvailable), {
            from: adminAccount
        })

        tokenSaleBalance = await token.balanceOf(tokenSale.address);
        assert.equal(tokenSaleBalance.toNumber(), tokensAvailable, 'Contract received funds.')

        await tokenSale.buyTokens(numberOfTokens, {
            from: buyerAccount,
            value: numberOfTokens * tokenPrice
        });

        // Grab the new balance of the buyerAccount
        buyerBalance = await token.balanceOf(buyerAccount);
        buyerBalance = buyerBalance.toNumber();
        assert.equal(buyerBalance, numberOfTokens, 'Buyer Balance updated');

        // Grab the new balance of the Token Sale Contract
        tokenSaleBalance = await token.balanceOf(tokenSale.address);
        tokenSaleBalance = tokenSaleBalance.toNumber();
        assert.equal(tokenSaleBalance, Number(tokensAvailable - numberOfTokens), 'Token Sale Balance updated');

    });
})

If we now our test, it should pass successfully.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

  Contract: NzouaTokenSale
    Contract Attributes
      ✓ Initializes the contract with the correct values
      ✓ References the ERC20 Token Contract (44ms)
      ✓ Sets the price of ERC20 Token Correctly (42ms)
    Facilitates Token Buying
      ✓ Keeps track of token sold (209ms)
      ✓ Triggers the Sell() Event (181ms)
      ✓ Requires that msg.value is equal to tokens to buy (43ms)
      ✓ Ensures contract has enough tokens (324ms)

  Contract: NzouaTokenSale - Successful Transfer
    ✓ Required a successful Transfer of Tokens (778ms)


  14 passing (5s)

Congratulations! We have now successfully written and tested the buyTokens() function, which is one of the main feature of a crowd sale smart contract. In the next section, we will learn how to end a crowd sale and return remaining tokens to the administrator of the crowd sale. Let’s get it!

End The Crowd Sale

In this section, we will learn how to close/end the token sale. To end the token sale we will ensure the following:

  • Require that only the administrator of the crowd sale can end the sale
  • Transfer the remaining tokens back to the administrator
  • Destroy/Deactivate the token sale contract

Require only Admin To End Token Sale

First, let’s declare a endSale() function inside ./contracts/NzouaTokenSale.sol

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{

        [... omitted code ...]

    }


    [... omitted code ...]

    // Ending the NzouaTokenSale sale
    function endSale() public {

        // Require only admin can end the sale

        // Transfer remaining tokens back to admin

        // Destroy/Deactivate the contract

    }

}

Next, we will write some tests scripts against the tasks defined above. To do this, we will declare a new contract() test function and we will call it NzouaTokenSale – End Token Sale. Go to ./test/NzouaTokenSale.test.js and add the following code:

var NzouaToken = artifacts.require('./NzouaToken');
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

});

contract('NzouaTokenSale - Successful Transfer', async (accounts) => {

    [... omitted code ...]

})

contract('NzouaTokenSale - End Token Sale', async(accounts) => {

    let tokenSale;
    let token;
    let tokenPrice = 1000000000000000; // in wei
    const adminAccount = accounts[0];
    const buyerAccount = accounts[1];

    beforeEach("setup all contracts", async () => {
        token = await NzouaToken.deployed();
        tokenSale = await NzouaTokenSale.deployed();
    });
    
})

Inside the contract(‘NzouaTokenSale – End Token Sale’, …) test block, we will add two tests entries.

  • One that will attempt to end the token sale as an account other than the buyer it(‘Cannot end token sale from account other than admin’, …)
  • One that will end the token sale as the administrator of the contract it(‘Ends the token sale from Admin’, …).
var NzouaToken = artifacts.require('./NzouaToken');
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

});

contract('NzouaTokenSale - Successful Transfer', async (accounts) => {

    [... omitted code ...]

})

contract('NzouaTokenSale - End Token Sale', async(accounts) => {

    let tokenSale;
    let token;
    let tokenPrice = 1000000000000000; // in wei
    const adminAccount = accounts[0];
    const buyerAccount = accounts[1];

    beforeEach("setup all contracts", async () => {
        token = await NzouaToken.deployed();
        tokenSale = await NzouaTokenSale.deployed();
    });

    it('Cannot end token sale from account other than admin', async () => {
        try {
            const receipt = await tokenSale.endSale({
                from: buyerAccount
            })
            assert.equal(receipt, false, 'Buyer can end the crowd sale')

        } catch (error) {
            // console.log(error.message)
            assert(error.message.indexOf('revert') >= 0, 'Buyer cannot end the crowd sale');
        }
    })
    it('Ends the token sale from Admin', async () => {
        const receipt = await tokenSale.endSale.call({
            from: adminAccount
        })
        assert(receipt, 'Buyer can end the crowd sale')

    })
    
})

If we run the test, it will fail, because one account trying to end the token sale is not an administrator.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale - Successful Transfer
    ✓ Required a successful Transfer of Tokens (715ms)

  Contract: NzouaTokenSale - End Token Sale
    1) Cannot end token sale from account other than admin
    > No events were emitted
    ✓ Ends the token sale from Admin


  15 passing (6s)
  1 failing

  1) Contract: NzouaTokenSale - End Token Sale
       Cannot end token sale from account other than admin:
     AssertionError: Buyer cannot end the crowd sale

[... omitted code ...]

To make this test pass, let’s add a require() statement inside the endSale() function. So go to ./contracts/NzouaTokenSale.sol and add the following code:

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{

        [... omitted code ...]

    }

    [... omitted code ...]

    // Ending the NzouaTokenSale sale
    function endSale() public {

        // Require only admin can end the sale
        require(msg.sender == admin, 'Only Admin can end the sale');

        // Transfer remaining tokens back to admin

        // Destroy/Deactivate the contract

    }

}

If we now run the test, it will pass as expected

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

  Contract: NzouaTokenSale - Successful Transfer
    ✓ Required a successful Transfer of Tokens (854ms)

  Contract: NzouaTokenSale - End Token Sale
    ✓ Cannot end token sale from account other than admin (53ms)
    ✓ Ends the token sale from Admin (100ms)


  16 passing (6s)

[... omitted output ...]

The next thing we need to do is ensure that the remaining tokens inside the crowd sale contract are sent back to the admin after the token sale has ended.

Send Remaining Tokens Back To Admin

The first step is to write our test script as usual. for this specific test, we will do it in sequence:

  • We will provision the Token Sale contract with tokens/funds (Transfer tokens from admin to contract)
  • We will execute the buyTokens() function by buyerAccount
  • We will execute the endSale() function by adminAccount
  • We will grab the new balance of adminAccount
  • We will confirm that the unsold tokens were returned to adminAccount

Go to ./test/NzouaTokenSale.test.js and inside contract(‘NzouaTokenSale – End Token Sale’…) test block, add the following code:

var NzouaToken = artifacts.require('./NzouaToken');
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

});

contract('NzouaTokenSale - Successful Transfer', async (accounts) => {

    [... omitted code ...]

})

contract('NzouaTokenSale - End Token Sale', async(accounts) => {

    let tokenSale;
    let token;
    let tokenPrice = 1000000000000000; // in wei
    const adminAccount = accounts[0];
    const buyerAccount = accounts[1];

    beforeEach("setup all contracts", async () => {
        token = await NzouaToken.deployed();
        tokenSale = await NzouaTokenSale.deployed();
    });

    [... omitted code ...]

    it('Ends the token sale from Admin', async () => {
        
        [... omitted code ...]

    })

    it('Sends remaining tokens back to Admin', async () => {
        let tokenSaleBalance;
        let adminBalance;

        const numberOfTokens = 10;
        const tokensAvailable = 750000;

        // Provisions Token Sale Contracts with Funds
        await token.transfer(tokenSale.address, (tokensAvailable), {
            from: adminAccount
        })

        // Simulate buyTokens() by a Buyer
        await tokenSale.buyTokens(numberOfTokens, {
            from: buyerAccount,
            value: numberOfTokens * tokenPrice
        });

        // End the token sale
        await tokenSale.endSale({
            from: adminAccount
        })

        // Grab the new balance of the adminAccount
        adminBalance = await token.balanceOf(adminAccount);
        adminBalance = adminBalance.toNumber();

        // Confirm the unsold tokens were returned to the admin
        assert.equal(adminBalance, 999990, 'Returns unsold tokens');
    })
    
})

If we run the test, it will fail because the unsold tokens were not returned to the admin. The contract expected the admin balance to be 999990, but got 250000

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

  Contract: NzouaTokenSale - End Token Sale
    ✓ Cannot end token sale from account other than admin
    ✓ Ends the token sale from Admin
    1) Sends remaining tokens back to Admin

    Events emitted during test:
    ---------------------------

    NzouaToken.Transfer(
      _from: <indexed> 0x5D16d433aFDB957ceF88231da5CDcf12b083E094 (type: address),
      _to: <indexed> 0x87246c23142062C11Db44473AA669FbD0196beCa (type: address),
      _value: 750000 (type: uint256)
    )

    NzouaToken.Transfer(
      _from: <indexed> 0x87246c23142062C11Db44473AA669FbD0196beCa (type: address),
      _to: <indexed> 0x838235F38b782Ce6d40e04469767842D91DfA162 (type: address),
      _value: 10 (type: uint256)
    )

    NzouaTokenSale.Sell(
      _buyer: 0x838235F38b782Ce6d40e04469767842D91DfA162 (type: address),
      _amount: 10 (type: uint256)
    )


    ---------------------------


  16 passing (6s)
  1 failing

  1) Contract: NzouaTokenSale - End Token Sale
       Sends remaining tokens back to Admin:

      Returns unsold tokens
      + expected - actual
      
      + 999990   - 250000

[... omitted output ...]

To fix this and make the test pass, we will add another require() statement to ensure that unsold tokens are transferred back to the administrator. Open ./contracts/NzouaTokenSale.sol and add the following inside the endSale() function:

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{

        [... omitted code ...]

    }

    [... omitted code ...]

    // Ending the NzouaTokenSale sale
    function endSale() public {

        // Require only admin can end the sale
        require(msg.sender == admin, 'Only Admin can end the sale');

        // Transfer remaining tokens back to admin
        require(tokenContract.transfer(admin, tokenContract.balanceOf(address(this))));

        // Destroy/Deactivate the contract

    }

}

If we now run the test, it will pas as expected

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

  Contract: NzouaTokenSale - End Token Sale
    ✓ Cannot end token sale from account other than admin (113ms)
    ✓ Ends the token sale from Admin (49ms)
    ✓ Sends remaining tokens back to Admin (605ms)


  17 passing (6s)

Destroy/Deactivate/Disable The Token Sale Contract

Smart contracts in solidity have a special function called selfdestruct, which is used by smart contract developers to clear contract’s data and move contract’s funds to a designated address. This function will put the contract in a useless/disabled state. Let’s implement that next.

To test if the token sale contract was successfully disabled, we will make sure that the balance of the token sale contract is reset to 0. Let’s open ./test/NouaTokenSale.test.js and add one last contract() test block that we will call NzouaTokenSale – Deactivation. This specific test will perform the following tasks:

  • Provision the token sale contract with funds from adminAccount
  • Record old balance of tokenSale contract
  • End the token sale
  • Request new balance of tokenSale contract
  • Confirm that oldBalance and newBalance of tokenSale contract have different values
  • Assert that newBalance of tokenSale contract was reset to 0 at the end of endSale() execution
var NzouaToken = artifacts.require('./NzouaToken');
var NzouaTokenSale = artifacts.require('./NzouaTokenSale');

contract('NzouaTokenSale', async (account) => {

    [... omitted code ...]

});

contract('NzouaTokenSale - Successful Transfer', async (accounts) => {

    [... omitted code ...]

})

contract('NzouaTokenSale - End Token Sale', async(accounts) => {

    [... omitted code ...]
    
})

contract('NzouaTokenSale - Deactivation', async (accounts) => {
    let tokenSale;
    let token;
    const adminAccount = accounts[0];
    const tokensAvailable = 750000;

    beforeEach("setup all contracts", async () => {
        token = await NzouaToken.deployed();
        tokenSale = await NzouaTokenSale.deployed();
    });

    it('Disables the token sale contract', async () => {

        // Provisions Token Sale Contracts with Funds
        await token.transfer(tokenSale.address, (tokensAvailable), {
            from: adminAccount
        })

        // Get the old balance of the token sale contract
        oldBalance = await token.balanceOf(tokenSale.address);

        // End the token sale
        await tokenSale.endSale({
            from: adminAccount
        })

        // Get the new balance of the token sale contract
        newBalance = await token.balanceOf(tokenSale.address);

        // Confirm that both old and new balances of the contract have different values
        assert.notEqual(oldBalance.toNumber(), newBalance.toNumber(), 'Both balances are not equal')

        // Confirm that the balance of the contract was reset after deactivation
        assert.equal(newBalance.toNumber(), 0, 'Balance was reset. Contract is disabled')
    })
})

If we run our test, it will fail, because it expect the tokenPrice variable to be 0.

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale - End Token Sale
    ✓ Cannot end token sale from account other than admin (122ms)
    ✓ Ends the token sale from Admin (47ms)
    ✓ Sends remaining tokens back to Admin (1007ms)
    1) Disables the token sale contract
    > No events were emitted


  17 passing (7s)
  1 failing

  1) Contract: NzouaTokenSale - End Token Sale
       Disables the token sale contract:

      Price was reset. Contract is disabled
      + expected - actual

      + 0        - 1000000000000000

[... omitted output...]

To make this final test pass, let’s call the built-in Solidity selfDestruct() function, inside the endSale() function. Go to ./contracts/NzouaTokenSale.sol and add the following code:

// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0;

// Import the ERC20 Token contract
import "./NzouaToken.sol";

// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaTokenSale {

    [... omitted code ...]

    constructor(NzouaToken _tokenContract, uint256 _tokenPrice) {

        [... omitted code ...]
    }

    // Buying tokens. This function is payable
    function buyTokens(uint256 _numberOfTokens) public payable{

        [... omitted code ...]

    }

    [... omitted code ...]

    // Ending the NzouaTokenSale sale
    function endSale() public {

        // Require only admin can end the sale
        require(msg.sender == admin, 'Only Admin can end the sale');

        // Transfer remaining tokens back to admin
        require(tokenContract.transfer(admin, tokenContract.balanceOf(address(this))));

        // Destroy/Deactivate the contract
        selfdestruct(payable(admin));

    }
}

Now the test will pass successfully

ERC20-Token $ truffle test --network ganache

// Output on the console

[... omitted output ...]

Contract: NzouaTokenSale - End Token Sale
    ✓ Cannot end token sale from account other than admin (205ms)
    ✓ Ends the token sale from Admin (51ms)
    ✓ Sends remaining tokens back to Admin (555ms)

  Contract: NzouaTokenSale - Deactivation
    ✓ Disables the token sale contract (544ms)


  18 passing (8s)

Congratulations! In this tutorial, We we able create an ERC-20 Token with Solidity. We also coded a separate smart contract to manage the sale of our tokens to the public via a crowd sale.

In order to deploy your smart contracts read this blog post which explains in details how to deploy on a local blockchain like Ganache, or a public test net such as Rinkeby.

In the last part of this three tutorial series, we will build a frontend that will allow us to interact with our smart contracts.

Nzouat
Nzouat
Software Architect & Full Stack Blockchain Developer
nzouat.com

Software Architect & Full Stack Blockchain Developer. I enjoy helping Entrepreneurs build the technology they need to run a successful business