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.