Simple ERC20 Crowdsale

The following is an example of a simple ERC20 Crowdsale.

:warning: The following code has not been tested nor audited.
Seek appropriate advice on regulatory compliance and your solution should have appropriate testing and auditing

Recommend reading: Points to consider when creating a fungible token (ERC20, ERC777)

Setup

Crowdsales are included in OpenZeppelin Contracts 2.x, they are not in OpenZeppelin Contracts 3.x.

To install:

npm install @openzeppelin/contracts@2.5.1

Documentation: https://docs.openzeppelin.com/contracts/2.x/crowdsales

SimpleToken.sol

// contracts/SimpleToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.5;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";

/**
 * @title SimpleToken
 * @dev Very simple ERC20 Token example, where all tokens are pre-assigned to the creator.
 * Note they can later distribute these tokens as they wish using `transfer` and other
 * `ERC20` functions.
 */
contract SimpleToken is Context, ERC20, ERC20Detailed {
    /**
     * @dev Constructor that gives _msgSender() all of existing tokens.
     */
    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) public ERC20Detailed(name, symbol, 18) {
        _mint(_msgSender(), initialSupply);
    }
}

SimpleCrowdsale.sol

// contracts/SimpleCrowdsale.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.5;

import "@openzeppelin/contracts/crowdsale/Crowdsale.sol";

/**
 * @title SimpleCrowdsale
 * @dev This is an example of a fully fledged crowdsale.
 */
contract SimpleCrowdsale is Crowdsale {
    constructor(
        uint256 rate,
        address payable wallet,
        IERC20 token
    ) public Crowdsale(rate, wallet, token) {}
}

2_deploy.js

Migrations script that deploys the token and crowdsale, then transfers the total supply of tokens to the crowdsale.

From: https://docs.openzeppelin.com/contracts/2.x/crowdsales#token-emission
In the default scenario, your crowdsale must own the tokens that are sold.

// migrations/2_deploy.js
// SPDX-License-Identifier: MIT
const SimpleToken = artifacts.require("SimpleToken");
const SimpleCrowdsale = artifacts.require("SimpleCrowdsale");

module.exports = async function (deployer, network, accounts) {
  await deployer.deploy(SimpleToken, 'Simple Token', 'SIM', '10000000000000000000000');
  const token = await SimpleToken.deployed();
  
  await deployer.deploy(SimpleCrowdsale, 1, accounts[0], token.address);
  const crowdsale = await SimpleCrowdsale.deployed();

  token.transfer(crowdsale.address, await token.totalSupply())
};

Truffle develop

Interact with the crowdsale using the Truffle console

$ npx truffle develop
Truffle Develop started at http://127.0.0.1:9545/
...
truffle(develop)> migrate

Compiling your contracts...
===========================
...
Starting migrations...
======================
> Network name:    'develop'
> Network id:      5777
> Block gas limit: 6721975 (0x6691b7)


1_initial_migration.js
...
2_deploy.js
===========

   Deploying 'SimpleToken'
...
   Deploying 'SimpleCrowdsale'
...
truffle(develop)> token = await SimpleToken.deployed()
undefined
truffle(develop)> crowdsale = await SimpleCrowdsale.deployed()
undefined
truffle(develop)> (await token.balanceOf(crowdsale.address)).toString()
'10000000000000000000000'
truffle(develop)> (await token.totalSupply()).toString()
'10000000000000000000000'
truffle(develop)> await crowdsale.buyTokens(accounts[1], {value: web3.utils.toWei('1')})
{ tx:
...
truffle(develop)> (await token.balanceOf(accounts[1])).toString()
'1000000000000000000'
truffle(develop)> (await token.balanceOf(crowdsale.address)).toString()
'9999000000000000000000'

Testing

Based on: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v2.5.1/test/examples/SampleCrowdsale.test.js

SimpleToken.test.js

// test/SimpleToken.test.js
// SPDX-License-Identifier: MIT

// Based on https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.5.1/test/examples/SimpleToken.test.js

const { expect } = require('chai');

// Import utilities from Test Helpers
const { BN, expectEvent, expectRevert, constants } = require('@openzeppelin/test-helpers');

// Load compiled artifacts
const SimpleToken = artifacts.require('SimpleToken');

// Start test block
contract('SimpleToken', function ([ creator, other ]) {

  const NAME = 'SimpleToken';
  const SYMBOL = 'SIM';
  const TOTAL_SUPPLY = new BN('10000000000000000000000');

  beforeEach(async function () {
    this.token = await SimpleToken.new(NAME, SYMBOL, TOTAL_SUPPLY, { from: creator });
  });

  it('retrieve returns a value previously stored', async function () {
    // Use large integer comparisons
    expect(await this.token.totalSupply()).to.be.bignumber.equal(TOTAL_SUPPLY);
  });

  it('has a name', async function () {
    expect(await this.token.name()).to.be.equal(NAME);
  });

  it('has a symbol', async function () {
    expect(await this.token.symbol()).to.be.equal(SYMBOL);
  });

  it('assigns the initial total supply to the creator', async function () {
    expect(await this.token.balanceOf(creator)).to.be.bignumber.equal(TOTAL_SUPPLY);
  });
});

SimpleCrowdsale.test.js

// test/SimpleCrowdsale.test.js
// SPDX-License-Identifier: MIT

// Based on https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.5.1/test/examples/SimpleToken.test.js

const { expect } = require('chai');

// Import utilities from Test Helpers
const { BN, ether, expectEvent, expectRevert, constants } = require('@openzeppelin/test-helpers');

// Load compiled artifacts
const SimpleToken = artifacts.require('SimpleToken');
const SimpleCrowdsale = artifacts.require('SimpleCrowdsale');

// Start test block
contract('SimpleCrowdsale', function ([ creator, investor, wallet ]) {

  const NAME = 'SimpleToken';
  const SYMBOL = 'SIM';
  const TOTAL_SUPPLY = new BN('10000000000000000000000');
  const RATE = new BN(10);

  beforeEach(async function () {
    this.token = await SimpleToken.new(NAME, SYMBOL, TOTAL_SUPPLY, { from: creator });
    this.crowdsale = await SimpleCrowdsale.new(RATE, wallet, this.token.address);
    this.token.transfer(this.crowdsale.address, await this.token.totalSupply());
  });

  it('should create crowdsale with correct parameters', async function () {
    expect(await this.crowdsale.rate()).to.be.bignumber.equal(RATE);
    expect(await this.crowdsale.wallet()).to.be.equal(wallet);
    expect(await this.crowdsale.token()).to.be.equal(this.token.address);
  });

  it('should accept payments', async function () {
    const investmentAmount = ether('1');
    const expectedTokenAmount = RATE.mul(investmentAmount);

    await this.crowdsale.buyTokens(investor, { value: investmentAmount, from: investor });

    expect(await this.token.balanceOf(investor)).to.be.bignumber.equal(expectedTokenAmount);
  });
});

Run Tests

$ npx truffle test
Using network 'test'.


Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: SimpleCrowdsale
    ✓ should create crowdsale with correct parameters (206ms)
    ✓ should accept payments (154ms)

  Contract: SimpleToken
    ✓ retrieve returns a value previously stored
    ✓ has a name (44ms)
    ✓ has a symbol (52ms)
    ✓ assigns the initial total supply to the creator


  6 passing (2s)
2 Likes

3 posts were split to a new topic: Use other addresses in tests