Build a Real World ICO - The Complete Walkthrough
By Gregory McCubbin Β·
Hey everybody, itβs Gregory from Dapp University!
Today I'm going to show you how to build a real world crowdsale that can be used to raise funds in a real world ICO. I'll show you how to build a production ready ERC-20 token and crowd sale with smart contracts that can be deployed to the Ethereum blockchain in order raise funds in an ICO. I'm going to walk you through each step in the process of developing these smart contracts, testing them, and deploying them to the live network!
This series is designed for developers who have experience with Solidity and are familiar with how ERC-20 tokens work. If you want a more beginner friendly tutorial, I highly recommend my 8-hour tutorial Code Your Own Cryptocurrency on Ethereum. That tutorial provides an complete explanation of how ERC-20 tokens work, and how to code a basic crowdsale smart contract. You can also download all the video content to the full 8-hour video series here for free π.
Also, if you're interested in launching an ICO, I have complete DONE FOR YOU service that includes:
- Custom ERC-20 token
- Crowdsale smart contracts
- ICO website
- KYC user registration
You can learn more about that service here.
You can download the full tutorial code to this project in order to follow along here on github.
Table of Contents
- Introduction & Setup
- Mintable Token
- Crowdsale Smart Contract
- Minted Crowdsale
- Capped Crowdsale
- Timed Crowdsale
- Whitelisted Crowdsale
- Refundable Crowdsale
- ICO Presale
- Finalize Crowdsale
- Token Distribution
- Token Vesting
- Deployment
ICO Success Checklist
Are you trying to figure out what you need in order to launch your ICO? Click the link below to get my checklist for ICO Success!
Get Checklist1. Introduction & Setup
This tutorial relies heavily on the OpenZeppelin Solidity framework for building the ERC-20 token and crowdsale smart contacts. This will allow us to start developing our smart contracts quickly without having to rebuild everything ourselves, and will also provide us with added security benefit, since the OpenZeppelin library is community vetted for security vulnerabilities. Check out the video above for a full explanation of how OpenZeppelin implements the ERC-20 standard and provides us with a starting point for building ICO smart contracts.
Now let's get our environment set up. First make sure you have Node.js installed on your computer. You can see if you have Node already installed by going to your terminal and typing:
$ node -v
The next dependency is the Truffle Framework, which provides a suite of tools that allow us to write smart contacts with the Solidity programming language. It also enables us to test our smart contracts and deploy them to the blockchain. It also gives us a place to develop our client-side application.
You can install Truffle with NPM by in your command line like this:
$ npm install -g [email protected]
Once you have Truffle installed, ensure that you're using version 4.1.11 like this:
$ truffle version
The next dependency is ganache-cli, which will give us a command line interface for running a private blockchain locally on our machine. We will use this private blockchain to run tests against the smart contracts we will develop in this tutorial.
$ npm install -g ganache-cli
Now that all of the dependencies are installed, let's create a new project directory. I'll call this ico_irl
, which stands for "ICO In Real Life", haha! Do that like this:
$ mkdir ico_irl
Now let's enter the newly created directory:
$ cd ico_irl
Now let's open a new terminal window (or pane/tab) and start ganache-cli
in order to run the private development blockchain:
$ ganache-cli
Now let's initialize a new Truffle project like this:
$ truffle init
Great! Now you've got a Truffle project set up. Next, let's install all of the Node.js packages we need to develop this project. Instead of installing them one-by-one, use this code in your package.json
file:
{
"name": "ico-irl",
"version": "1.0.0",
"description": "Real World ICO",
"main": "truffle.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-stage-2": "^6.18.0",
"babel-preset-stage-3": "^6.17.0",
"babel-register": "^6.26.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.0.0",
"chai-bignumber": "^2.0.2",
"coveralls": "^3.0.1",
"dotenv": "^4.0.0",
"eslint": "^4.19.1",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"ethereumjs-util": "^5.2.0",
"ethjs-abi": "^0.2.1",
"ganache-cli": "6.1.0",
"openzeppelin-solidity": "1.10.0",
"solidity-coverage": "^0.5.4",
"solium": "^1.1.7",
"truffle": "4.1.11",
"truffle-hdwallet-provider": "0.0.5",
"web3-utils": "^1.0.0-beta.34"
}
}
Next, let's update our Truffle config file. If you're on windows, you need to edit truffle-config.js
. If you're on a Mac or Linux machine, use truffle.js
. Your configuration file should look like this:
require('babel-register');
require('babel-polyfill');
require('dotenv').config();
const HDWalletProvider = require('truffle-hdwallet-provider');
module.exports = {
networks: {
development: {
host: 'localhost',
port: 8545,
network_id: '*', // eslint-disable-line camelcase
},
ganache: {
host: 'localhost',
port: 8545,
network_id: '*', // eslint-disable-line camelcase
},
ropsten: {
provider: function() {
return new HDWalletProvider(
process.env.MNEMONIC,
`https://ropsten.infura.io/${process.env.INFURA_API_KEY}`
)
},
gas: 5000000,
gasPrice: 25000000000,
network_id: 3
}
},
solc: {
optimizer: {
enabled: true,
runs: 200
}
}
};
Next, create a .env
file to store environment variables. We'll set 2 environment variables here. The first is an infura API key. Infura is a service that provides free access to hosted Ethereum nodes to make it easy to connect to the Ethereum network. You can obtain an Infura API key for free here. Next, you'll need a mnemonic seed phrase used to generate an Ethereum account, along with some fake test Ether on the Ropsten Test Network. We'll use this account to deploy smart contracts. Once you have those things, your .env
file should look like this:
INFURA_API_KEY=abc123 // Add your key here
MNEMONIC="football mortgage apple..." // Add your mnemonic here
Now let's create a new file for the token smart contract.
$ touch contracts/DappToken.sol
Inside this file, we'll use this code to get started with the token:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol";
import "openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol";
contract DappToken is StandardToken, DetailedERC20 {
constructor(string _name, string _symbol, uint8 _decimals)
DetailedERC20(_name, _symbol, _decimals)
public
{
}
}
Let me explain this code:
- First, we import the 2 OpenZeppelin libraries we need to build the token:
DetailedERC20
andStandardToken
.StadardToken
provides all of the basic ERC-20 functions for us, andDetailedERC20
allows us to pass some constructor arguments to customize the token. - Next, create a new smart contract called
DappToken
that inherits from both of these libraries. - Next, create a constructor that gets run whenever the contract is migrated. This constructor takes arguments that customize the token. These arguments get passed into the
DetailedERC20
contract.
And that's it! We've already got a basic ERC-20 token. Now let's compile the smart contracts to ensure that everything worked properly, and that we have no code errors:
$ truffle compile
Now your contract should compile successfully. If you ran into any errors, you can download the full tutorial code to this project to check your work here on github.
Want to Hire an Expert?
If you're interested in hiring an ICO expert, I can lead your project step-by-step from "zero to ICO" with my battle tested ICO solution!
Learn More2. Mintable Token
Now we want to add some more behavior to our token. First, we want to make the token "mintable", which means we want to be able to create new tokens. This will allow us to create new tokens in the crowdsale, instead of having a fixed total supply from the beginning. Next, we want to make our token "pausable". This will allow us to freeze token transfers during the crowdsale so that investors cannot dump them while other people are still buying them. We can simply change the contracts that our token contract inherits from and modify our token code to look like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol";
import "openzeppelin-solidity/contracts/token/ERC20/PausableToken.sol";
contract DappToken is MintableToken, PausableToken, DetailedERC20 {
constructor(string _name, string _symbol, uint8 _decimals)
DetailedERC20(_name, _symbol, _decimals)
public
{
}
}
Now let's set up a basic test to ensure that our token behaves the way we expect. We can create a new file like this:
$ touch test/DappToken.test.js
We'll set up a new test inside the newly created file like this:
const BigNumber = web3.BigNumber;
const DappToken = artifacts.require('DappToken');
require('chai')
.use(require('chai-bignumber')(BigNumber))
.should();
contract('DappToken', accounts => {
const _name = 'Dapp Token';
const _symbol = 'DAPP';
const _decimals = 18;
beforeEach(async function () {
this.token = await DappToken.new(_name, _symbol, _decimals);
});
describe('token attributes', function() {
it('has the correct name', async function() {
const name = await this.token.name();
name.should.equal(_name);
});
it('has the correct symbol', async function() {
const symbol = await this.token.symbol();
symbol.should.equal(_symbol);
});
it('has the correct decimals', async function() {
const decimals = await this.token.decimals();
decimals.should.be.bignumber.equal(_decimals);
});
});
});
Now run the tests like this (make sure you have ganache-cli
running):
$ truffle test
YAY! They passed! π For a more in-depth explanation, you can watch me build out the code for the Mintable/Pausable token, as well as setting up the test suite in Video #2 above.
3. Crowdsale Smart Contract
Now let's start building the crowdsale smart contract. Let's create a file for this code:
$ touch contracts/DappTokenCrowdsale.sol
Inside here, we'll create the crowdsale smart conract:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
contract DappTokenCrowdsale is Crowdsale {
constructor(
uint256 _rate,
address _wallet,
ERC20 _token
)
Crowdsale(_rate, _wallet, _token)
public
{
}
}
Let me explain this code. First, we import the Crowdsale
smart contract library from OpenZeppelin, and we inherit from it in our contract. Next, we add some constructor arguments to our smart contract and pass them to the Crowdsale
smart contract library. Here is an explanation of the arguments:
rate
- the rate at which tokens are purchased in the crowdsale. If the rate is1
, then1 wei
buys 1 token. If the rate is500
, then500 wei
buys 1 token.wallet
- this is the account where Ether funds are sent in the ICO.token
- this is the address of the ERC-20 token being sold in the crowdsale.
Now let's set up a test for the crowdsale. First, we'll create a test file:
$ touch test/DappTokenSale.test.js
Now, let's fill in the code for this test:
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address
);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
});
4. Minted Crowdsale
Now, let's build our crowdsale to mint tokens whenever someone purchases them. We can update our smart contract to look like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale {
constructor(
uint256 _rate,
address _wallet,
ERC20 _token
)
Crowdsale(_rate, _wallet, _token)
public
{
}
}
Let's write some tests for this new behavior. In order to do this, I'm going to create some new helpers that we'll use for this test, and also more tests later on in this tutorial. I'll walk you through creating those helpers step by step right now:
First, we'll create a helper for converting Ether to wei:
$ touch test/helpers/ether.js
Inside here, paste the following code:
export default function ether (n) {
return new web3.BigNumber(web3.toWei(n, 'ether'));
}
Next, we'll create a helper to test for EVM failures:
$ touch test/helpers/EVMRevert.js
Inside here, paste the following code:
export default 'revert';
Next, we'll create a helper to get the current blockchain time:
$ touch test/helpers/latestTime.js
Inside here, paste the following code:
// Returns the time of the last mined block in seconds
export default function latestTime () {
return web3.eth.getBlock('latest').timestamp;
}
Last, we'll create a to change the current blockchain time:
$ touch test/helpers/increaseTime.js
Inside here, paste the following code:
import latestTime from './latestTime';
// Increases ganache time by the passed duration in seconds
export default function increaseTime (duration) {
const id = Date.now();
return new Promise((resolve, reject) => {
web3.currentProvider.sendAsync({
jsonrpc: '2.0',
method: 'evm_increaseTime',
params: [duration],
id: id,
}, err1 => {
if (err1) return reject(err1);
web3.currentProvider.sendAsync({
jsonrpc: '2.0',
method: 'evm_mine',
id: id + 1,
}, (err2, res) => {
return err2 ? reject(err2) : resolve(res);
});
});
});
}
/**
* Beware that due to the need of calling two separate ganache methods and rpc calls overhead
* it's hard to increase time precisely to a target point so design your test to tolerate
* small fluctuations from time to time.
*
* @param target time in seconds
*/
export function increaseTimeTo (target) {
let now = latestTime();
if (target < now) throw Error(`Cannot increase current time(${now}) to a moment in the past(${target})`);
let diff = target - now;
return increaseTime(diff);
}
export const duration = {
seconds: function (val) { return val; },
minutes: function (val) { return val * this.seconds(60); },
hours: function (val) { return val * this.minutes(60); },
days: function (val) { return val * this.hours(24); },
weeks: function (val) { return val * this.days(7); },
years: function (val) { return val * this.days(365); },
};
Now, with all these helpers in place, we can add tests for our minted crowdsale. Your test file should look like this:
import ether from './helpers/ether';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address
);
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
Want to Hire an Expert?
If you're interested in hiring an ICO expert, I can lead your project step-by-step from "zero to ICO" with my battle tested ICO solution!
Learn More5. Capped Crowdsale
Now let's implement a "cap" or limit on our crowdsale. We'll create 2 limits. First, we'll create a hard cap for the maximum amount of Ether raised in the crowdsale. This will be the cap
variable. Next, we'll create a "minum cap" which will represent the minimum Ether contribution we will accept from each investor. We can configure our smart contract to look like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale, CappedCrowdsale {
// Track investor contributions
uint256 public investorMinCap = 2000000000000000; // 0.002 ether
uint256 public investorHardCap = 50000000000000000000; // 50 ether
mapping(address => uint256) public contributions;
constructor(
uint256 _rate,
address _wallet,
ERC20 _token,
uint256 _cap
)
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap)
public
{
}
/**
* @dev Returns the amount contributed so far by a sepecific user.
* @param _beneficiary Address of contributor
* @return User contribution so far
*/
function getUserContribution(address _beneficiary)
public view returns (uint256)
{
return contributions[_beneficiary];
}
/**
* @dev Extend parent behavior requiring purchase to respect investor min/max funding cap.
* @param _beneficiary Token purchaser
* @param _weiAmount Amount of wei contributed
*/
function _preValidatePurchase(
address _beneficiary,
uint256 _weiAmount
)
internal
{
super._preValidatePurchase(_beneficiary, _weiAmount);
uint256 _existingContribution = contributions[_beneficiary];
uint256 _newContribution = _existingContribution.add(_weiAmount);
require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);
contributions[_beneficiary] = _newContribution;
}
}
Now we can update our test file to check for this behavior like this:
import ether from './helpers/ether';
import EVMRevert from './helpers/EVMRevert';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.cap = ether(100);
// Investor caps
this.investorMinCap = ether(0.002);
this.inestorHardCap = ether(50);
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address,
this.cap
);
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('capped crowdsale', async function() {
it('has the correct hard cap', async function() {
const cap = await this.crowdsale.cap();
cap.should.be.bignumber.equal(this.cap);
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
describe('buyTokens()', function() {
describe('when the contribution is less than the minimum cap', function() {
it('rejects the transaction', async function() {
const value = this.investorMinCap - 1;
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the investor has already met the minimum cap', function() {
it('allows the investor to contribute below the minimum cap', async function() {
// First contribution is valid
const value1 = ether(1);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution is less than investor cap
const value2 = 1; // wei
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.fulfilled;
});
});
});
describe('when the total contributions exceed the investor hard cap', function () {
it('rejects the transaction', async function () {
// First contribution is in valid range
const value1 = ether(2);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution sends total contributions over investor hard cap
const value2 = ether(49);
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the contribution is within the valid range', function () {
const value = ether(2);
it('succeeds & updates the contribution amount', async function () {
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.fulfilled;
const contribution = await this.crowdsale.getUserContribution(investor2);
contribution.should.be.bignumber.equal(value);
});
});
});
6. Timed Crowdsale
Now we can add a timer to our crowdsale. We'll add an opening time and a closing time. We will only allow investors to purchase tokens within this time window. We can update our smart contract to look like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/TimedCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale, CappedCrowdsale, TimedCrowdsale {
// Track investor contributions
uint256 public investorMinCap = 2000000000000000; // 0.002 ether
uint256 public investorHardCap = 50000000000000000000; // 50 ether
mapping(address => uint256) public contributions;
constructor(
uint256 _rate,
address _wallet,
ERC20 _token,
uint256 _cap,
uint256 _openingTime,
uint256 _closingTime
)
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap)
TimedCrowdsale(_openingTime, _closingTime)
public
{
}
/**
* @dev Returns the amount contributed so far by a sepecific user.
* @param _beneficiary Address of contributor
* @return User contribution so far
*/
function getUserContribution(address _beneficiary)
public view returns (uint256)
{
return contributions[_beneficiary];
}
/**
* @dev Extend parent behavior requiring purchase to respect investor min/max funding cap.
* @param _beneficiary Token purchaser
* @param _weiAmount Amount of wei contributed
*/
function _preValidatePurchase(
address _beneficiary,
uint256 _weiAmount
)
internal
{
super._preValidatePurchase(_beneficiary, _weiAmount);
uint256 _existingContribution = contributions[_beneficiary];
uint256 _newContribution = _existingContribution.add(_weiAmount);
require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);
contributions[_beneficiary] = _newContribution;
}
}
And we can test for this behavior like this:
import ether from './helpers/ether';
import EVMRevert from './helpers/EVMRevert';
import { increaseTimeTo, duration } from './helpers/increaseTime';
import latestTime from './helpers/latestTime';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.cap = ether(100);
this.openingTime = latestTime() + duration.weeks(1);
this.closingTime = this.openingTime + duration.weeks(1);
// Investor caps
this.investorMinCap = ether(0.002);
this.inestorHardCap = ether(50);
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address,
this.cap,
this.openingTime,
this.closingTime
);
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
// Advance time to crowdsale start
await increaseTimeTo(this.openingTime + 1);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('capped crowdsale', async function() {
it('has the correct hard cap', async function() {
const cap = await this.crowdsale.cap();
cap.should.be.bignumber.equal(this.cap);
});
});
describe('timed crowdsale', function() {
it('is open', async function() {
const isClosed = await this.crowdsale.hasClosed();
isClosed.should.be.false;
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
describe('buyTokens()', function() {
describe('when the contribution is less than the minimum cap', function() {
it('rejects the transaction', async function() {
const value = this.investorMinCap - 1;
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the investor has already met the minimum cap', function() {
it('allows the investor to contribute below the minimum cap', async function() {
// First contribution is valid
const value1 = ether(1);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution is less than investor cap
const value2 = 1; // wei
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.fulfilled;
});
});
});
describe('when the total contributions exceed the investor hard cap', function () {
it('rejects the transaction', async function () {
// First contribution is in valid range
const value1 = ether(2);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution sends total contributions over investor hard cap
const value2 = ether(49);
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the contribution is within the valid range', function () {
const value = ether(2);
it('succeeds & updates the contribution amount', async function () {
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.fulfilled;
const contribution = await this.crowdsale.getUserContribution(investor2);
contribution.should.be.bignumber.equal(value);
});
});
});
7. Whitelisted Crowdsale
Now let's turn our smart contract into a whitelisted crowdsale. By doing this, we'll create a white list that restricts the accounts that can contribute to the crowdsale. We'll also add the ability to add investors to the whielist so that they can contribute. We can add that to the smart contract like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/TimedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/WhitelistedCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale, CappedCrowdsale, TimedCrowdsale, WhitelistedCrowdsale {
// Track investor contributions
uint256 public investorMinCap = 2000000000000000; // 0.002 ether
uint256 public investorHardCap = 50000000000000000000; // 50 ether
mapping(address => uint256) public contributions;
constructor(
uint256 _rate,
address _wallet,
ERC20 _token,
uint256 _cap,
uint256 _openingTime,
uint256 _closingTime
)
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap)
TimedCrowdsale(_openingTime, _closingTime)
public
{
}
/**
* @dev Returns the amount contributed so far by a sepecific user.
* @param _beneficiary Address of contributor
* @return User contribution so far
*/
function getUserContribution(address _beneficiary)
public view returns (uint256)
{
return contributions[_beneficiary];
}
/**
* @dev Extend parent behavior requiring purchase to respect investor min/max funding cap.
* @param _beneficiary Token purchaser
* @param _weiAmount Amount of wei contributed
*/
function _preValidatePurchase(
address _beneficiary,
uint256 _weiAmount
)
internal
{
super._preValidatePurchase(_beneficiary, _weiAmount);
uint256 _existingContribution = contributions[_beneficiary];
uint256 _newContribution = _existingContribution.add(_weiAmount);
require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);
contributions[_beneficiary] = _newContribution;
}
}
We can also test for this new whitelisting behavior like this:
import ether from './helpers/ether';
import EVMRevert from './helpers/EVMRevert';
import { increaseTimeTo, duration } from './helpers/increaseTime';
import latestTime from './helpers/latestTime';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.cap = ether(100);
this.openingTime = latestTime() + duration.weeks(1);
this.closingTime = this.openingTime + duration.weeks(1);
// Investor caps
this.investorMinCap = ether(0.002);
this.inestorHardCap = ether(50);
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address,
this.cap,
this.openingTime,
this.closingTime
);
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
// Add investors to whitelist
await this.crowdsale.addAddressesToWhitelist([investor1, investor2]);
// Advance time to crowdsale start
await increaseTimeTo(this.openingTime + 1);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('capped crowdsale', async function() {
it('has the correct hard cap', async function() {
const cap = await this.crowdsale.cap();
cap.should.be.bignumber.equal(this.cap);
});
});
describe('timed crowdsale', function() {
it('is open', async function() {
const isClosed = await this.crowdsale.hasClosed();
isClosed.should.be.false;
});
});
describe('whitelisted crowdsale', function() {
it('rejects contributions from non-whitelisted investors', async function() {
const notWhitelisted = _;
await this.crowdsale.buyTokens(notWhitelisted, { value: ether(1), from: notWhitelisted }).should.be.rejectedWith(EVMRevert);
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
describe('buyTokens()', function() {
describe('when the contribution is less than the minimum cap', function() {
it('rejects the transaction', async function() {
const value = this.investorMinCap - 1;
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the investor has already met the minimum cap', function() {
it('allows the investor to contribute below the minimum cap', async function() {
// First contribution is valid
const value1 = ether(1);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution is less than investor cap
const value2 = 1; // wei
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.fulfilled;
});
});
});
describe('when the total contributions exceed the investor hard cap', function () {
it('rejects the transaction', async function () {
// First contribution is in valid range
const value1 = ether(2);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution sends total contributions over investor hard cap
const value2 = ether(49);
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the contribution is within the valid range', function () {
const value = ether(2);
it('succeeds & updates the contribution amount', async function () {
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.fulfilled;
const contribution = await this.crowdsale.getUserContribution(investor2);
contribution.should.be.bignumber.equal(value);
});
});
});
8. Refundable Crowdsale
Now let's add refund support to the crowdsale smart contract. With this feature, we'll create a fund raising goal. If the goal is met, the wallet will get to keep the funds, and investors will have tokens. If the goal is not met, investors will be able to claim refunds. During the crowdsale, all funds will be locked into a refund vault. We can update the smart contract to look like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/TimedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/WhitelistedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/distribution/RefundableCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale, CappedCrowdsale, TimedCrowdsale, WhitelistedCrowdsale, RefundableCrowdsale {
// Track investor contributions
uint256 public investorMinCap = 2000000000000000; // 0.002 ether
uint256 public investorHardCap = 50000000000000000000; // 50 ether
mapping(address => uint256) public contributions;
constructor(
uint256 _rate,
address _wallet,
ERC20 _token,
uint256 _cap,
uint256 _openingTime,
uint256 _closingTime,
uint256 _goal
)
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap)
TimedCrowdsale(_openingTime, _closingTime)
RefundableCrowdsale(_goal)
public
{
require(_goal <= _cap);
}
/**
* @dev Returns the amount contributed so far by a sepecific user.
* @param _beneficiary Address of contributor
* @return User contribution so far
*/
function getUserContribution(address _beneficiary)
public view returns (uint256)
{
return contributions[_beneficiary];
}
/**
* @dev Extend parent behavior requiring purchase to respect investor min/max funding cap.
* @param _beneficiary Token purchaser
* @param _weiAmount Amount of wei contributed
*/
function _preValidatePurchase(
address _beneficiary,
uint256 _weiAmount
)
internal
{
super._preValidatePurchase(_beneficiary, _weiAmount);
uint256 _existingContribution = contributions[_beneficiary];
uint256 _newContribution = _existingContribution.add(_weiAmount);
require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);
contributions[_beneficiary] = _newContribution;
}
}
Now we can test for this behavior like this:
import ether from './helpers/ether';
import EVMRevert from './helpers/EVMRevert';
import { increaseTimeTo, duration } from './helpers/increaseTime';
import latestTime from './helpers/latestTime';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
const RefundVault = artifacts.require('./RefundVault');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.cap = ether(100);
this.openingTime = latestTime() + duration.weeks(1);
this.closingTime = this.openingTime + duration.weeks(1);
this.goal = ether(50);
// Investor caps
this.investorMinCap = ether(0.002);
this.inestorHardCap = ether(50);
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address,
this.cap,
this.openingTime,
this.closingTime,
this.goal
);
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
// Add investors to whitelist
await this.crowdsale.addManyToWhitelist([investor1, investor2]);
// Track refund vault
this.vaultAddress = await this.crowdsale.vault();
this.vault = RefundVault.at(this.vaultAddress);
// Advance time to crowdsale start
await increaseTimeTo(this.openingTime + 1);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('capped crowdsale', async function() {
it('has the correct hard cap', async function() {
const cap = await this.crowdsale.cap();
cap.should.be.bignumber.equal(this.cap);
});
});
describe('timed crowdsale', function() {
it('is open', async function() {
const isClosed = await this.crowdsale.hasClosed();
isClosed.should.be.false;
});
});
describe('whitelisted crowdsale', function() {
it('rejects contributions from non-whitelisted investors', async function() {
const notWhitelisted = _;
await this.crowdsale.buyTokens(notWhitelisted, { value: ether(1), from: notWhitelisted }).should.be.rejectedWith(EVMRevert);
});
});
describe('refundable crowdsale', function() {
beforeEach(async function() {
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
describe('during crowdsale', function() {
it('prevents the investor from claiming refund', async function() {
await this.vault.refund(investor1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
describe('buyTokens()', function() {
describe('when the contribution is less than the minimum cap', function() {
it('rejects the transaction', async function() {
const value = this.investorMinCap - 1;
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the investor has already met the minimum cap', function() {
it('allows the investor to contribute below the minimum cap', async function() {
// First contribution is valid
const value1 = ether(1);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution is less than investor cap
const value2 = 1; // wei
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.fulfilled;
});
});
});
describe('when the total contributions exceed the investor hard cap', function () {
it('rejects the transaction', async function () {
// First contribution is in valid range
const value1 = ether(2);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution sends total contributions over investor hard cap
const value2 = ether(49);
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the contribution is within the valid range', function () {
const value = ether(2);
it('succeeds & updates the contribution amount', async function () {
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.fulfilled;
const contribution = await this.crowdsale.getUserContribution(investor2);
contribution.should.be.bignumber.equal(value);
});
});
});
9. ICO Presale
Now let's add a feature to create an ICO presale. This will allow us to add phases to our crowdsale. When the ICO is in "presale" mode, all funds will go directly to the wallet instead of the refund vault. We can add this feature to our smart contract like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/TimedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/WhitelistedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/distribution/RefundableCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale, CappedCrowdsale, TimedCrowdsale, WhitelistedCrowdsale, RefundableCrowdsale {
// Track investor contributions
uint256 public investorMinCap = 2000000000000000; // 0.002 ether
uint256 public investorHardCap = 50000000000000000000; // 50 ether
mapping(address => uint256) public contributions;
// Crowdsale Stages
enum CrowdsaleStage { PreICO, ICO }
// Default to presale stage
CrowdsaleStage public stage = CrowdsaleStage.PreICO;
constructor(
uint256 _rate,
address _wallet,
ERC20 _token,
uint256 _cap,
uint256 _openingTime,
uint256 _closingTime,
uint256 _goal
)
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap)
TimedCrowdsale(_openingTime, _closingTime)
RefundableCrowdsale(_goal)
public
{
require(_goal <= _cap);
}
/**
* @dev Returns the amount contributed so far by a sepecific user.
* @param _beneficiary Address of contributor
* @return User contribution so far
*/
function getUserContribution(address _beneficiary)
public view returns (uint256)
{
return contributions[_beneficiary];
}
/**
* @dev Allows admin to update the crowdsale stage
* @param _stage Crowdsale stage
*/
function setCrowdsaleStage(uint _stage) public onlyOwner {
if(uint(CrowdsaleStage.PreICO) == _stage) {
stage = CrowdsaleStage.PreICO;
} else if (uint(CrowdsaleStage.ICO) == _stage) {
stage = CrowdsaleStage.ICO;
}
if(stage == CrowdsaleStage.PreICO) {
rate = 500;
} else if (stage == CrowdsaleStage.ICO) {
rate = 250;
}
}
/**
* @dev forwards funds to the wallet during the PreICO stage, then the refund vault during ICO stage
*/
function _forwardFunds() internal {
if(stage == CrowdsaleStage.PreICO) {
wallet.transfer(msg.value);
} else if (stage == CrowdsaleStage.ICO) {
super._forwardFunds();
}
}
/**
* @dev Extend parent behavior requiring purchase to respect investor min/max funding cap.
* @param _beneficiary Token purchaser
* @param _weiAmount Amount of wei contributed
*/
function _preValidatePurchase(
address _beneficiary,
uint256 _weiAmount
)
internal
{
super._preValidatePurchase(_beneficiary, _weiAmount);
uint256 _existingContribution = contributions[_beneficiary];
uint256 _newContribution = _existingContribution.add(_weiAmount);
require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);
contributions[_beneficiary] = _newContribution;
}
}
Now we can test for this behavior like this:
import ether from './helpers/ether';
import EVMRevert from './helpers/EVMRevert';
import { increaseTimeTo, duration } from './helpers/increaseTime';
import latestTime from './helpers/latestTime';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
const RefundVault = artifacts.require('./RefundVault');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.cap = ether(100);
this.openingTime = latestTime() + duration.weeks(1);
this.closingTime = this.openingTime + duration.weeks(1);
this.goal = ether(50);
// Investor caps
this.investorMinCap = ether(0.002);
this.inestorHardCap = ether(50);
// ICO Stages
this.preIcoStage = 0;
this.preIcoRate = 500;
this.icoStage = 1;
this.icoRate = 250;
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address,
this.cap,
this.openingTime,
this.closingTime,
this.goal
);
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
// Add investors to whitelist
await this.crowdsale.addManyToWhitelist([investor1, investor2]);
// Track refund vault
this.vaultAddress = await this.crowdsale.vault();
this.vault = RefundVault.at(this.vaultAddress);
// Advance time to crowdsale start
await increaseTimeTo(this.openingTime + 1);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('capped crowdsale', async function() {
it('has the correct hard cap', async function() {
const cap = await this.crowdsale.cap();
cap.should.be.bignumber.equal(this.cap);
});
});
describe('timed crowdsale', function() {
it('is open', async function() {
const isClosed = await this.crowdsale.hasClosed();
isClosed.should.be.false;
});
});
describe('whitelisted crowdsale', function() {
it('rejects contributions from non-whitelisted investors', async function() {
const notWhitelisted = _;
await this.crowdsale.buyTokens(notWhitelisted, { value: ether(1), from: notWhitelisted }).should.be.rejectedWith(EVMRevert);
});
});
describe('refundable crowdsale', function() {
beforeEach(async function() {
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
describe('during crowdsale', function() {
it('prevents the investor from claiming refund', async function() {
await this.vault.refund(investor1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the corwdsale stage is PreICO', function() {
beforeEach(async function () {
// Crowdsale stage is already PreICO by default
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
it('forwards funds to the wallet', async function () {
const balance = await web3.eth.getBalance(this.wallet);
expect(balance.toNumber()).to.be.above(ether(100));
});
});
describe('when the crowdsale stage is ICO', function() {
beforeEach(async function () {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: _ });
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
it('forwards funds to the refund vault', async function () {
const balance = await web3.eth.getBalance(this.vaultAddress);
expect(balance.toNumber()).to.be.above(0);
});
});
});
describe('crowdsale stages', function() {
it('it starts in PreICO', async function () {
const stage = await this.crowdsale.stage();
stage.should.be.bignumber.equal(this.preIcoStage);
});
it('starts at the preICO rate', async function () {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.preIcoRate);
});
it('allows admin to update the stage & rate', async function() {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: _ });
const stage = await this.crowdsale.stage();
stage.should.be.bignumber.equal(this.icoStage);
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.icoRate);
});
it('prevents non-admin from updating the stage', async function () {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
describe('buyTokens()', function() {
describe('when the contribution is less than the minimum cap', function() {
it('rejects the transaction', async function() {
const value = this.investorMinCap - 1;
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the investor has already met the minimum cap', function() {
it('allows the investor to contribute below the minimum cap', async function() {
// First contribution is valid
const value1 = ether(1);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution is less than investor cap
const value2 = 1; // wei
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.fulfilled;
});
});
});
describe('when the total contributions exceed the investor hard cap', function () {
it('rejects the transaction', async function () {
// First contribution is in valid range
const value1 = ether(2);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution sends total contributions over investor hard cap
const value2 = ether(49);
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the contribution is within the valid range', function () {
const value = ether(2);
it('succeeds & updates the contribution amount', async function () {
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.fulfilled;
const contribution = await this.crowdsale.getUserContribution(investor2);
contribution.should.be.bignumber.equal(value);
});
});
});
10. Finalize Crowdsale
Now let's add a feature to finalize the crowdsale. We'll create a new function that allows us to do this, and we'll build out this function in the next few sections. First, we'll finish minting tokens so that no more tokens can be minted after the crowdsale is over. Next, we'll unpause the token. We'll only do these things if the crowdsale goal is reached. We can update our code to look like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-solidity/contracts/token/ERC20/PausableToken.sol";
import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol";
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/TimedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/WhitelistedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/distribution/RefundableCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale, CappedCrowdsale, TimedCrowdsale, WhitelistedCrowdsale, RefundableCrowdsale {
// Track investor contributions
uint256 public investorMinCap = 2000000000000000; // 0.002 ether
uint256 public investorHardCap = 50000000000000000000; // 50 ether
mapping(address => uint256) public contributions;
// Crowdsale Stages
enum CrowdsaleStage { PreICO, ICO }
// Default to presale stage
CrowdsaleStage public stage = CrowdsaleStage.PreICO;
constructor(
uint256 _rate,
address _wallet,
ERC20 _token,
uint256 _cap,
uint256 _openingTime,
uint256 _closingTime,
uint256 _goal
)
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap)
TimedCrowdsale(_openingTime, _closingTime)
RefundableCrowdsale(_goal)
public
{
require(_goal <= _cap);
}
/**
* @dev Returns the amount contributed so far by a sepecific user.
* @param _beneficiary Address of contributor
* @return User contribution so far
*/
function getUserContribution(address _beneficiary)
public view returns (uint256)
{
return contributions[_beneficiary];
}
/**
* @dev Allows admin to update the crowdsale stage
* @param _stage Crowdsale stage
*/
function setCrowdsaleStage(uint _stage) public onlyOwner {
if(uint(CrowdsaleStage.PreICO) == _stage) {
stage = CrowdsaleStage.PreICO;
} else if (uint(CrowdsaleStage.ICO) == _stage) {
stage = CrowdsaleStage.ICO;
}
if(stage == CrowdsaleStage.PreICO) {
rate = 500;
} else if (stage == CrowdsaleStage.ICO) {
rate = 250;
}
}
/**
* @dev forwards funds to the wallet during the PreICO stage, then the refund vault during ICO stage
*/
function _forwardFunds() internal {
if(stage == CrowdsaleStage.PreICO) {
wallet.transfer(msg.value);
} else if (stage == CrowdsaleStage.ICO) {
super._forwardFunds();
}
}
/**
* @dev Extend parent behavior requiring purchase to respect investor min/max funding cap.
* @param _beneficiary Token purchaser
* @param _weiAmount Amount of wei contributed
*/
function _preValidatePurchase(
address _beneficiary,
uint256 _weiAmount
)
internal
{
super._preValidatePurchase(_beneficiary, _weiAmount);
uint256 _existingContribution = contributions[_beneficiary];
uint256 _newContribution = _existingContribution.add(_weiAmount);
require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);
contributions[_beneficiary] = _newContribution;
}
/**
* @dev enables token transfers, called when owner calls finalize()
*/
function finalization() internal {
if(goalReached()) {
MintableToken _mintableToken = MintableToken(token);
// Do more stuff....
_mintableToken.finishMinting();
// Unpause the token
PausableToken(token).unpause();
}
super.finalization();
}
}
Now we can test for this behavior like this:
import ether from './helpers/ether';
import EVMRevert from './helpers/EVMRevert';
import { increaseTimeTo, duration } from './helpers/increaseTime';
import latestTime from './helpers/latestTime';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
const RefundVault = artifacts.require('./RefundVault');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
before(async function() {
// Transfer extra ether to investor1's account for testing
await web3.eth.sendTransaction({ from: _, to: investor1, value: ether(25) })
});
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.cap = ether(100);
this.openingTime = latestTime() + duration.weeks(1);
this.closingTime = this.openingTime + duration.weeks(1);
this.goal = ether(50);
// Investor caps
this.investorMinCap = ether(0.002);
this.inestorHardCap = ether(50);
// ICO Stages
this.preIcoStage = 0;
this.preIcoRate = 500;
this.icoStage = 1;
this.icoRate = 250;
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address,
this.cap,
this.openingTime,
this.closingTime,
this.goal
);
// Pause Token
await this.token.pause();
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
// Add investors to whitelist
await this.crowdsale.addManyToWhitelist([investor1, investor2]);
// Track refund vault
this.vaultAddress = await this.crowdsale.vault();
this.vault = RefundVault.at(this.vaultAddress);
// Advance time to crowdsale start
await increaseTimeTo(this.openingTime + 1);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('capped crowdsale', async function() {
it('has the correct hard cap', async function() {
const cap = await this.crowdsale.cap();
cap.should.be.bignumber.equal(this.cap);
});
});
describe('timed crowdsale', function() {
it('is open', async function() {
const isClosed = await this.crowdsale.hasClosed();
isClosed.should.be.false;
});
});
describe('whitelisted crowdsale', function() {
it('rejects contributions from non-whitelisted investors', async function() {
const notWhitelisted = _;
await this.crowdsale.buyTokens(notWhitelisted, { value: ether(1), from: notWhitelisted }).should.be.rejectedWith(EVMRevert);
});
});
describe('refundable crowdsale', function() {
beforeEach(async function() {
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
describe('during crowdsale', function() {
it('prevents the investor from claiming refund', async function() {
await this.vault.refund(investor1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the corwdsale stage is PreICO', function() {
beforeEach(async function () {
// Crowdsale stage is already PreICO by default
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
it('forwards funds to the wallet', async function () {
const balance = await web3.eth.getBalance(this.wallet);
expect(balance.toNumber()).to.be.above(ether(100));
});
});
describe('when the crowdsale stage is ICO', function() {
beforeEach(async function () {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: _ });
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
it('forwards funds to the refund vault', async function () {
const balance = await web3.eth.getBalance(this.vaultAddress);
expect(balance.toNumber()).to.be.above(0);
});
});
});
describe('crowdsale stages', function() {
it('it starts in PreICO', async function () {
const stage = await this.crowdsale.stage();
stage.should.be.bignumber.equal(this.preIcoStage);
});
it('starts at the preICO rate', async function () {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.preIcoRate);
});
it('allows admin to update the stage & rate', async function() {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: _ });
const stage = await this.crowdsale.stage();
stage.should.be.bignumber.equal(this.icoStage);
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.icoRate);
});
it('prevents non-admin from updating the stage', async function () {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
describe('buyTokens()', function() {
describe('when the contribution is less than the minimum cap', function() {
it('rejects the transaction', async function() {
const value = this.investorMinCap - 1;
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the investor has already met the minimum cap', function() {
it('allows the investor to contribute below the minimum cap', async function() {
// First contribution is valid
const value1 = ether(1);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution is less than investor cap
const value2 = 1; // wei
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.fulfilled;
});
});
});
describe('when the total contributions exceed the investor hard cap', function () {
it('rejects the transaction', async function () {
// First contribution is in valid range
const value1 = ether(2);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution sends total contributions over investor hard cap
const value2 = ether(49);
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the contribution is within the valid range', function () {
const value = ether(2);
it('succeeds & updates the contribution amount', async function () {
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.fulfilled;
const contribution = await this.crowdsale.getUserContribution(investor2);
contribution.should.be.bignumber.equal(value);
});
});
describe('token transfers', function () {
it('does not allow investors to transfer tokens during crowdsale', async function () {
// Buy some tokens first
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
// Attempt to transfer tokens during crowdsale
await this.token.transfer(investor2, 1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('finalizing the crowdsale', function() {
describe('when the goal is not reached', function() {
beforeEach(async function () {
// Do not meet the toal
await this.crowdsale.buyTokens(investor2, { value: ether(1), from: investor2 });
// Fastforward past end time
await increaseTimeTo(this.closingTime + 1);
// Finalize the crowdsale
await this.crowdsale.finalize({ from: _ });
});
it('allows the investor to claim refund', async function () {
await this.vault.refund(investor2, { from: investor2 }).should.be.fulfilled;
});
});
describe('when the goal is reached', function() {
beforeEach(async function () {
// track current wallet balance
this.walletBalance = await web3.eth.getBalance(wallet);
// Meet the goal
await this.crowdsale.buyTokens(investor1, { value: ether(26), from: investor1 });
await this.crowdsale.buyTokens(investor2, { value: ether(26), from: investor2 });
// Fastforward past end time
await increaseTimeTo(this.closingTime + 1);
// Finalize the crowdsale
await this.crowdsale.finalize({ from: _ });
});
it('handles goal reached', async function () {
// Tracks goal reached
const goalReached = await this.crowdsale.goalReached();
goalReached.should.be.true;
// Finishes minting token
const mintingFinished = await this.token.mintingFinished();
mintingFinished.should.be.true;
// Unpauses the token
const paused = await this.token.paused();
paused.should.be.false;
// Prevents investor from claiming refund
await this.vault.refund(investor1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
});
});
11. Token Distribution
Now let's add a feature to distribute tokens whenever the crowdsale is finalized. This will determine the economics of our token. We'll define percentages in our smart contract and like this:
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-solidity/contracts/token/ERC20/PausableToken.sol";
import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol";
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/TimedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/WhitelistedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/distribution/RefundableCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale, CappedCrowdsale, TimedCrowdsale, WhitelistedCrowdsale, RefundableCrowdsale {
// Track investor contributions
uint256 public investorMinCap = 2000000000000000; // 0.002 ether
uint256 public investorHardCap = 50000000000000000000; // 50 ether
mapping(address => uint256) public contributions;
// Crowdsale Stages
enum CrowdsaleStage { PreICO, ICO }
// Default to presale stage
CrowdsaleStage public stage = CrowdsaleStage.PreICO;
// Token Distribution
uint256 public tokenSalePercentage = 70;
uint256 public foundersPercentage = 10;
uint256 public foundationPercentage = 10;
uint256 public partnersPercentage = 10;
constructor(
uint256 _rate,
address _wallet,
ERC20 _token,
uint256 _cap,
uint256 _openingTime,
uint256 _closingTime,
uint256 _goal
)
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap)
TimedCrowdsale(_openingTime, _closingTime)
RefundableCrowdsale(_goal)
public
{
require(_goal <= _cap);
}
/**
* @dev Returns the amount contributed so far by a sepecific user.
* @param _beneficiary Address of contributor
* @return User contribution so far
*/
function getUserContribution(address _beneficiary)
public view returns (uint256)
{
return contributions[_beneficiary];
}
/**
* @dev Allows admin to update the crowdsale stage
* @param _stage Crowdsale stage
*/
function setCrowdsaleStage(uint _stage) public onlyOwner {
if(uint(CrowdsaleStage.PreICO) == _stage) {
stage = CrowdsaleStage.PreICO;
} else if (uint(CrowdsaleStage.ICO) == _stage) {
stage = CrowdsaleStage.ICO;
}
if(stage == CrowdsaleStage.PreICO) {
rate = 500;
} else if (stage == CrowdsaleStage.ICO) {
rate = 250;
}
}
/**
* @dev forwards funds to the wallet during the PreICO stage, then the refund vault during ICO stage
*/
function _forwardFunds() internal {
if(stage == CrowdsaleStage.PreICO) {
wallet.transfer(msg.value);
} else if (stage == CrowdsaleStage.ICO) {
super._forwardFunds();
}
}
/**
* @dev Extend parent behavior requiring purchase to respect investor min/max funding cap.
* @param _beneficiary Token purchaser
* @param _weiAmount Amount of wei contributed
*/
function _preValidatePurchase(
address _beneficiary,
uint256 _weiAmount
)
internal
{
super._preValidatePurchase(_beneficiary, _weiAmount);
uint256 _existingContribution = contributions[_beneficiary];
uint256 _newContribution = _existingContribution.add(_weiAmount);
require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);
contributions[_beneficiary] = _newContribution;
}
/**
* @dev enables token transfers, called when owner calls finalize()
*/
function finalization() internal {
if(goalReached()) {
MintableToken _mintableToken = MintableToken(token);
// Distribute tokens...
_mintableToken.finishMinting();
// Unpause the token
PausableToken _pausableToken = PausableToken(token);
_pausableToken.unpause();
_pausableToken.transferOwnership(wallet);
}
super.finalization();
}
}
Now we can test for this behavior like this:
import ether from './helpers/ether';
import EVMRevert from './helpers/EVMRevert';
import { increaseTimeTo, duration } from './helpers/increaseTime';
import latestTime from './helpers/latestTime';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
const RefundVault = artifacts.require('./RefundVault');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2]) {
before(async function() {
// Transfer extra ether to investor1's account for testing
await web3.eth.sendTransaction({ from: _, to: investor1, value: ether(25) })
});
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.cap = ether(100);
this.openingTime = latestTime() + duration.weeks(1);
this.closingTime = this.openingTime + duration.weeks(1);
this.goal = ether(50);
// Investor caps
this.investorMinCap = ether(0.002);
this.inestorHardCap = ether(50);
// ICO Stages
this.preIcoStage = 0;
this.preIcoRate = 500;
this.icoStage = 1;
this.icoRate = 250;
// Token Distribution
this.tokenSalePercentage = 70;
this.foundersPercentage = 10;
this.foundationPercentage = 10;
this.partnersPercentage = 10;
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address,
this.cap,
this.openingTime,
this.closingTime,
this.goal
);
// Pause Token
await this.token.pause();
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
// Add investors to whitelist
await this.crowdsale.addManyToWhitelist([investor1, investor2]);
// Track refund vault
this.vaultAddress = await this.crowdsale.vault();
this.vault = RefundVault.at(this.vaultAddress);
// Advance time to crowdsale start
await increaseTimeTo(this.openingTime + 1);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('capped crowdsale', async function() {
it('has the correct hard cap', async function() {
const cap = await this.crowdsale.cap();
cap.should.be.bignumber.equal(this.cap);
});
});
describe('timed crowdsale', function() {
it('is open', async function() {
const isClosed = await this.crowdsale.hasClosed();
isClosed.should.be.false;
});
});
describe('whitelisted crowdsale', function() {
it('rejects contributions from non-whitelisted investors', async function() {
const notWhitelisted = _;
await this.crowdsale.buyTokens(notWhitelisted, { value: ether(1), from: notWhitelisted }).should.be.rejectedWith(EVMRevert);
});
});
describe('refundable crowdsale', function() {
beforeEach(async function() {
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
describe('during crowdsale', function() {
it('prevents the investor from claiming refund', async function() {
await this.vault.refund(investor1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the corwdsale stage is PreICO', function() {
beforeEach(async function () {
// Crowdsale stage is already PreICO by default
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
it('forwards funds to the wallet', async function () {
const balance = await web3.eth.getBalance(this.wallet);
expect(balance.toNumber()).to.be.above(ether(100));
});
});
describe('when the crowdsale stage is ICO', function() {
beforeEach(async function () {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: _ });
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
it('forwards funds to the refund vault', async function () {
const balance = await web3.eth.getBalance(this.vaultAddress);
expect(balance.toNumber()).to.be.above(0);
});
});
});
describe('crowdsale stages', function() {
it('it starts in PreICO', async function () {
const stage = await this.crowdsale.stage();
stage.should.be.bignumber.equal(this.preIcoStage);
});
it('starts at the preICO rate', async function () {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.preIcoRate);
});
it('allows admin to update the stage & rate', async function() {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: _ });
const stage = await this.crowdsale.stage();
stage.should.be.bignumber.equal(this.icoStage);
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.icoRate);
});
it('prevents non-admin from updating the stage', async function () {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
describe('buyTokens()', function() {
describe('when the contribution is less than the minimum cap', function() {
it('rejects the transaction', async function() {
const value = this.investorMinCap - 1;
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the investor has already met the minimum cap', function() {
it('allows the investor to contribute below the minimum cap', async function() {
// First contribution is valid
const value1 = ether(1);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution is less than investor cap
const value2 = 1; // wei
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.fulfilled;
});
});
});
describe('when the total contributions exceed the investor hard cap', function () {
it('rejects the transaction', async function () {
// First contribution is in valid range
const value1 = ether(2);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution sends total contributions over investor hard cap
const value2 = ether(49);
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the contribution is within the valid range', function () {
const value = ether(2);
it('succeeds & updates the contribution amount', async function () {
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.fulfilled;
const contribution = await this.crowdsale.getUserContribution(investor2);
contribution.should.be.bignumber.equal(value);
});
});
describe('token transfers', function () {
it('does not allow investors to transfer tokens during crowdsale', async function () {
// Buy some tokens first
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
// Attempt to transfer tokens during crowdsale
await this.token.transfer(investor2, 1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('finalizing the crowdsale', function() {
describe('when the goal is not reached', function() {
beforeEach(async function () {
// Do not meet the toal
await this.crowdsale.buyTokens(investor2, { value: ether(1), from: investor2 });
// Fastforward past end time
await increaseTimeTo(this.closingTime + 1);
// Finalize the crowdsale
await this.crowdsale.finalize({ from: _ });
});
it('allows the investor to claim refund', async function () {
await this.vault.refund(investor2, { from: investor2 }).should.be.fulfilled;
});
});
describe('when the goal is reached', function() {
beforeEach(async function () {
// track current wallet balance
this.walletBalance = await web3.eth.getBalance(wallet);
// Meet the goal
await this.crowdsale.buyTokens(investor1, { value: ether(26), from: investor1 });
await this.crowdsale.buyTokens(investor2, { value: ether(26), from: investor2 });
// Fastforward past end time
await increaseTimeTo(this.closingTime + 1);
// Finalize the crowdsale
await this.crowdsale.finalize({ from: _ });
});
it('handles goal reached', async function () {
// Tracks goal reached
const goalReached = await this.crowdsale.goalReached();
goalReached.should.be.true;
// Finishes minting token
const mintingFinished = await this.token.mintingFinished();
mintingFinished.should.be.true;
// Unpauses the token
const paused = await this.token.paused();
paused.should.be.false;
// Transfers ownership to the wallet
const owner = await this.token.owner();
owner.should.equal(this.wallet);
// Prevents investor from claiming refund
await this.vault.refund(investor1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
});
describe('token distribution', function() {
it('tracks token distribution correctly', async function () {
const tokenSalePercentage = await this.crowdsale.tokenSalePercentage();
tokenSalePercentage.should.be.bignumber.eq(this.tokenSalePercentage, 'has correct tokenSalePercentage');
const foundersPercentage = await this.crowdsale.foundersPercentage();
foundersPercentage.should.be.bignumber.eq(this.foundersPercentage, 'has correct foundersPercentage');
const foundationPercentage = await this.crowdsale.foundationPercentage();
foundationPercentage.should.be.bignumber.eq(this.foundationPercentage, 'has correct foundationPercentage');
const partnersPercentage = await this.crowdsale.partnersPercentage();
partnersPercentage.should.be.bignumber.eq(this.partnersPercentage, 'has correct partnersPercentage');
});
it('is a valid percentage breakdown', async function () {
const tokenSalePercentage = await this.crowdsale.tokenSalePercentage();
const foundersPercentage = await this.crowdsale.foundersPercentage();
const foundationPercentage = await this.crowdsale.foundationPercentage();
const partnersPercentage = await this.crowdsale.partnersPercentage();
const total = tokenSalePercentage.toNumber() + foundersPercentage.toNumber() + foundationPercentage.toNumber() + partnersPercentage.toNumber()
total.should.equal(100);
});
});
});
12. Token Vesting
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-solidity/contracts/token/ERC20/PausableToken.sol";
import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol";
import "openzeppelin-solidity/contracts/token/ERC20/TokenTimelock.sol";
import "openzeppelin-solidity/contracts/crowdsale/Crowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/CappedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/TimedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/validation/WhitelistedCrowdsale.sol";
import "openzeppelin-solidity/contracts/crowdsale/distribution/RefundableCrowdsale.sol";
contract DappTokenCrowdsale is Crowdsale, MintedCrowdsale, CappedCrowdsale, TimedCrowdsale, WhitelistedCrowdsale, RefundableCrowdsale {
// Track investor contributions
uint256 public investorMinCap = 2000000000000000; // 0.002 ether
uint256 public investorHardCap = 50000000000000000000; // 50 ether
mapping(address => uint256) public contributions;
// Crowdsale Stages
enum CrowdsaleStage { PreICO, ICO }
// Default to presale stage
CrowdsaleStage public stage = CrowdsaleStage.PreICO;
// Token Distribution
uint256 public tokenSalePercentage = 70;
uint256 public foundersPercentage = 10;
uint256 public foundationPercentage = 10;
uint256 public partnersPercentage = 10;
// Token reserve funds
address public foundersFund;
address public foundationFund;
address public partnersFund;
// Token time lock
uint256 public releaseTime;
address public foundersTimelock;
address public foundationTimelock;
address public partnersTimelock;
constructor(
uint256 _rate,
address _wallet,
ERC20 _token,
uint256 _cap,
uint256 _openingTime,
uint256 _closingTime,
uint256 _goal,
address _foundersFund,
address _foundationFund,
address _partnersFund,
uint256 _releaseTime
)
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap)
TimedCrowdsale(_openingTime, _closingTime)
RefundableCrowdsale(_goal)
public
{
require(_goal <= _cap);
foundersFund = _foundersFund;
foundationFund = _foundationFund;
partnersFund = _partnersFund;
releaseTime = _releaseTime;
}
/**
* @dev Returns the amount contributed so far by a sepecific user.
* @param _beneficiary Address of contributor
* @return User contribution so far
*/
function getUserContribution(address _beneficiary)
public view returns (uint256)
{
return contributions[_beneficiary];
}
/**
* @dev Allows admin to update the crowdsale stage
* @param _stage Crowdsale stage
*/
function setCrowdsaleStage(uint _stage) public onlyOwner {
if(uint(CrowdsaleStage.PreICO) == _stage) {
stage = CrowdsaleStage.PreICO;
} else if (uint(CrowdsaleStage.ICO) == _stage) {
stage = CrowdsaleStage.ICO;
}
if(stage == CrowdsaleStage.PreICO) {
rate = 500;
} else if (stage == CrowdsaleStage.ICO) {
rate = 250;
}
}
/**
* @dev forwards funds to the wallet during the PreICO stage, then the refund vault during ICO stage
*/
function _forwardFunds() internal {
if(stage == CrowdsaleStage.PreICO) {
wallet.transfer(msg.value);
} else if (stage == CrowdsaleStage.ICO) {
super._forwardFunds();
}
}
/**
* @dev Extend parent behavior requiring purchase to respect investor min/max funding cap.
* @param _beneficiary Token purchaser
* @param _weiAmount Amount of wei contributed
*/
function _preValidatePurchase(
address _beneficiary,
uint256 _weiAmount
)
internal
{
super._preValidatePurchase(_beneficiary, _weiAmount);
uint256 _existingContribution = contributions[_beneficiary];
uint256 _newContribution = _existingContribution.add(_weiAmount);
require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);
contributions[_beneficiary] = _newContribution;
}
/**
* @dev enables token transfers, called when owner calls finalize()
*/
function finalization() internal {
if(goalReached()) {
MintableToken _mintableToken = MintableToken(token);
uint256 _alreadyMinted = _mintableToken.totalSupply();
uint256 _finalTotalSupply = _alreadyMinted.div(tokenSalePercentage).mul(100);
foundersTimelock = new TokenTimelock(token, foundersFund, releaseTime);
foundationTimelock = new TokenTimelock(token, foundationFund, releaseTime);
partnersTimelock = new TokenTimelock(token, partnersFund, releaseTime);
_mintableToken.mint(address(foundersTimelock), _finalTotalSupply.mul(foundersPercentage).div(100));
_mintableToken.mint(address(foundationTimelock), _finalTotalSupply.mul(foundationPercentage).div(100));
_mintableToken.mint(address(partnersTimelock), _finalTotalSupply.mul(partnersPercentage).div(100));
_mintableToken.finishMinting();
// Unpause the token
PausableToken _pausableToken = PausableToken(token);
_pausableToken.unpause();
_pausableToken.transferOwnership(wallet);
}
super.finalization();
}
}
Now we can test for this behavior like this:
import ether from './helpers/ether';
import EVMRevert from './helpers/EVMRevert';
import { increaseTimeTo, duration } from './helpers/increaseTime';
import latestTime from './helpers/latestTime';
const BigNumber = web3.BigNumber;
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
const DappToken = artifacts.require('DappToken');
const DappTokenCrowdsale = artifacts.require('DappTokenCrowdsale');
const RefundVault = artifacts.require('./RefundVault');
const TokenTimelock = artifacts.require('./TokenTimelock');
contract('DappTokenCrowdsale', function([_, wallet, investor1, investor2, foundersFund, foundationFund, partnersFund]) {
before(async function() {
// Transfer extra ether to investor1's account for testing
await web3.eth.sendTransaction({ from: _, to: investor1, value: ether(25) })
});
beforeEach(async function () {
// Token config
this.name = "DappToken";
this.symbol = "DAPP";
this.decimals = 18;
// Deploy Token
this.token = await DappToken.new(
this.name,
this.symbol,
this.decimals
);
// Crowdsale config
this.rate = 500;
this.wallet = wallet;
this.cap = ether(100);
this.openingTime = latestTime() + duration.weeks(1);
this.closingTime = this.openingTime + duration.weeks(1);
this.goal = ether(50);
this.foundersFund = foundersFund;
this.foundationFund = foundationFund;
this.partnersFund = partnersFund;
this.releaseTime = this.closingTime + duration.years(1);
// Investor caps
this.investorMinCap = ether(0.002);
this.inestorHardCap = ether(50);
// ICO Stages
this.preIcoStage = 0;
this.preIcoRate = 500;
this.icoStage = 1;
this.icoRate = 250;
// Token Distribution
this.tokenSalePercentage = 70;
this.foundersPercentage = 10;
this.foundationPercentage = 10;
this.partnersPercentage = 10;
this.crowdsale = await DappTokenCrowdsale.new(
this.rate,
this.wallet,
this.token.address,
this.cap,
this.openingTime,
this.closingTime,
this.goal,
this.foundersFund,
this.foundationFund,
this.partnersFund,
this.releaseTime
);
// Pause Token
await this.token.pause();
// Transfer token ownership to crowdsale
await this.token.transferOwnership(this.crowdsale.address);
// Add investors to whitelist
await this.crowdsale.addManyToWhitelist([investor1, investor2]);
// Track refund vault
this.vaultAddress = await this.crowdsale.vault();
this.vault = RefundVault.at(this.vaultAddress);
// Advance time to crowdsale start
await increaseTimeTo(this.openingTime + 1);
});
describe('crowdsale', function() {
it('tracks the rate', async function() {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.rate);
});
it('tracks the wallet', async function() {
const wallet = await this.crowdsale.wallet();
wallet.should.equal(this.wallet);
});
it('tracks the token', async function() {
const token = await this.crowdsale.token();
token.should.equal(this.token.address);
});
});
describe('minted crowdsale', function() {
it('mints tokens after purchase', async function() {
const originalTotalSupply = await this.token.totalSupply();
await this.crowdsale.sendTransaction({ value: ether(1), from: investor1 });
const newTotalSupply = await this.token.totalSupply();
assert.isTrue(newTotalSupply > originalTotalSupply);
});
});
describe('capped crowdsale', async function() {
it('has the correct hard cap', async function() {
const cap = await this.crowdsale.cap();
cap.should.be.bignumber.equal(this.cap);
});
});
describe('timed crowdsale', function() {
it('is open', async function() {
const isClosed = await this.crowdsale.hasClosed();
isClosed.should.be.false;
});
});
describe('whitelisted crowdsale', function() {
it('rejects contributions from non-whitelisted investors', async function() {
const notWhitelisted = _;
await this.crowdsale.buyTokens(notWhitelisted, { value: ether(1), from: notWhitelisted }).should.be.rejectedWith(EVMRevert);
});
});
describe('refundable crowdsale', function() {
beforeEach(async function() {
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
describe('during crowdsale', function() {
it('prevents the investor from claiming refund', async function() {
await this.vault.refund(investor1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the corwdsale stage is PreICO', function() {
beforeEach(async function () {
// Crowdsale stage is already PreICO by default
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
it('forwards funds to the wallet', async function () {
const balance = await web3.eth.getBalance(this.wallet);
expect(balance.toNumber()).to.be.above(ether(100));
});
});
describe('when the crowdsale stage is ICO', function() {
beforeEach(async function () {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: _ });
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
});
it('forwards funds to the refund vault', async function () {
const balance = await web3.eth.getBalance(this.vaultAddress);
expect(balance.toNumber()).to.be.above(0);
});
});
});
describe('crowdsale stages', function() {
it('it starts in PreICO', async function () {
const stage = await this.crowdsale.stage();
stage.should.be.bignumber.equal(this.preIcoStage);
});
it('starts at the preICO rate', async function () {
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.preIcoRate);
});
it('allows admin to update the stage & rate', async function() {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: _ });
const stage = await this.crowdsale.stage();
stage.should.be.bignumber.equal(this.icoStage);
const rate = await this.crowdsale.rate();
rate.should.be.bignumber.equal(this.icoRate);
});
it('prevents non-admin from updating the stage', async function () {
await this.crowdsale.setCrowdsaleStage(this.icoStage, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('accepting payments', function() {
it('should accept payments', async function() {
const value = ether(1);
const purchaser = investor2;
await this.crowdsale.sendTransaction({ value: value, from: investor1 }).should.be.fulfilled;
await this.crowdsale.buyTokens(investor1, { value: value, from: purchaser }).should.be.fulfilled;
});
});
describe('buyTokens()', function() {
describe('when the contribution is less than the minimum cap', function() {
it('rejects the transaction', async function() {
const value = this.investorMinCap - 1;
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the investor has already met the minimum cap', function() {
it('allows the investor to contribute below the minimum cap', async function() {
// First contribution is valid
const value1 = ether(1);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution is less than investor cap
const value2 = 1; // wei
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.fulfilled;
});
});
});
describe('when the total contributions exceed the investor hard cap', function () {
it('rejects the transaction', async function () {
// First contribution is in valid range
const value1 = ether(2);
await this.crowdsale.buyTokens(investor1, { value: value1, from: investor1 });
// Second contribution sends total contributions over investor hard cap
const value2 = ether(49);
await this.crowdsale.buyTokens(investor1, { value: value2, from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('when the contribution is within the valid range', function () {
const value = ether(2);
it('succeeds & updates the contribution amount', async function () {
await this.crowdsale.buyTokens(investor2, { value: value, from: investor2 }).should.be.fulfilled;
const contribution = await this.crowdsale.getUserContribution(investor2);
contribution.should.be.bignumber.equal(value);
});
});
describe('token transfers', function () {
it('does not allow investors to transfer tokens during crowdsale', async function () {
// Buy some tokens first
await this.crowdsale.buyTokens(investor1, { value: ether(1), from: investor1 });
// Attempt to transfer tokens during crowdsale
await this.token.transfer(investor2, 1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
describe('finalizing the crowdsale', function() {
describe('when the goal is not reached', function() {
beforeEach(async function () {
// Do not meet the toal
await this.crowdsale.buyTokens(investor2, { value: ether(1), from: investor2 });
// Fastforward past end time
await increaseTimeTo(this.closingTime + 1);
// Finalize the crowdsale
await this.crowdsale.finalize({ from: _ });
});
it('allows the investor to claim refund', async function () {
await this.vault.refund(investor2, { from: investor2 }).should.be.fulfilled;
});
});
describe('when the goal is reached', function() {
beforeEach(async function () {
// track current wallet balance
this.walletBalance = await web3.eth.getBalance(wallet);
// Meet the goal
await this.crowdsale.buyTokens(investor1, { value: ether(26), from: investor1 });
await this.crowdsale.buyTokens(investor2, { value: ether(26), from: investor2 });
// Fastforward past end time
await increaseTimeTo(this.closingTime + 1);
// Finalize the crowdsale
await this.crowdsale.finalize({ from: _ });
});
it('handles goal reached', async function () {
// Tracks goal reached
const goalReached = await this.crowdsale.goalReached();
goalReached.should.be.true;
// Finishes minting token
const mintingFinished = await this.token.mintingFinished();
mintingFinished.should.be.true;
// Unpauses the token
const paused = await this.token.paused();
paused.should.be.false;
// Enables token transfers
await this.token.transfer(investor2, 1, { from: investor2 }).should.be.fulfilled;
let totalSupply = await this.token.totalSupply();
totalSupply = totalSupply.toString();
// Founders
const foundersTimelockAddress = await this.crowdsale.foundersTimelock();
let foundersTimelockBalance = await this.token.balanceOf(foundersTimelockAddress);
foundersTimelockBalance = foundersTimelockBalance.toString();
foundersTimelockBalance = foundersTimelockBalance / (10 ** this.decimals);
let foundersAmount = totalSupply / this.foundersPercentage;
foundersAmount = foundersAmount.toString();
foundersAmount = foundersAmount / (10 ** this.decimals);
assert.equal(foundersTimelockBalance.toString(), foundersAmount.toString());
// Foundation
const foundationTimelockAddress = await this.crowdsale.foundationTimelock();
let foundationTimelockBalance = await this.token.balanceOf(foundationTimelockAddress);
foundationTimelockBalance = foundationTimelockBalance.toString();
foundationTimelockBalance = foundationTimelockBalance / (10 ** this.decimals);
let foundationAmount = totalSupply / this.foundationPercentage;
foundationAmount = foundationAmount.toString();
foundationAmount = foundationAmount / (10 ** this.decimals);
assert.equal(foundationTimelockBalance.toString(), foundationAmount.toString());
// Partners
const partnersTimelockAddress = await this.crowdsale.partnersTimelock();
let partnersTimelockBalance = await this.token.balanceOf(partnersTimelockAddress);
partnersTimelockBalance = partnersTimelockBalance.toString();
partnersTimelockBalance = partnersTimelockBalance / (10 ** this.decimals);
let partnersAmount = totalSupply / this.partnersPercentage;
partnersAmount = partnersAmount.toString();
partnersAmount = partnersAmount / (10 ** this.decimals);
assert.equal(partnersTimelockBalance.toString(), partnersAmount.toString());
// Can't withdraw from timelocks
const foundersTimelock = await TokenTimelock.at(foundersTimelockAddress);
await foundersTimelock.release().should.be.rejectedWith(EVMRevert);
const foundationTimelock = await TokenTimelock.at(foundationTimelockAddress);
await foundationTimelock.release().should.be.rejectedWith(EVMRevert);
const partnersTimelock = await TokenTimelock.at(partnersTimelockAddress);
await partnersTimelock.release().should.be.rejectedWith(EVMRevert);
// Can withdraw from timelocks
await increaseTimeTo(this.releaseTime + 1);
await foundersTimelock.release().should.be.fulfilled;
await foundationTimelock.release().should.be.fulfilled;
await partnersTimelock.release().should.be.fulfilled;
// Funds now have balances
// Founders
let foundersBalance = await this.token.balanceOf(this.foundersFund);
foundersBalance = foundersBalance.toString();
foundersBalance = foundersBalance / (10 ** this.decimals);
assert.equal(foundersBalance.toString(), foundersAmount.toString());
// Foundation
let foundationBalance = await this.token.balanceOf(this.foundationFund);
foundationBalance = foundationBalance.toString();
foundationBalance = foundationBalance / (10 ** this.decimals);
assert.equal(foundationBalance.toString(), foundationAmount.toString());
// Partners
let partnersBalance = await this.token.balanceOf(this.partnersFund);
partnersBalance = partnersBalance.toString();
partnersBalance = partnersBalance / (10 ** this.decimals);
assert.equal(partnersBalance.toString(), partnersAmount.toString());
// Transfers ownership to the wallet
const owner = await this.token.owner();
owner.should.equal(this.wallet);
// Prevents investor from claiming refund
await this.vault.refund(investor1, { from: investor1 }).should.be.rejectedWith(EVMRevert);
});
});
});
describe('token distribution', function() {
it('tracks token distribution correctly', async function () {
const tokenSalePercentage = await this.crowdsale.tokenSalePercentage();
tokenSalePercentage.should.be.bignumber.eq(this.tokenSalePercentage, 'has correct tokenSalePercentage');
const foundersPercentage = await this.crowdsale.foundersPercentage();
foundersPercentage.should.be.bignumber.eq(this.foundersPercentage, 'has correct foundersPercentage');
const foundationPercentage = await this.crowdsale.foundationPercentage();
foundationPercentage.should.be.bignumber.eq(this.foundationPercentage, 'has correct foundationPercentage');
const partnersPercentage = await this.crowdsale.partnersPercentage();
partnersPercentage.should.be.bignumber.eq(this.partnersPercentage, 'has correct partnersPercentage');
});
it('is a valid percentage breakdown', async function () {
const tokenSalePercentage = await this.crowdsale.tokenSalePercentage();
const foundersPercentage = await this.crowdsale.foundersPercentage();
const foundationPercentage = await this.crowdsale.foundationPercentage();
const partnersPercentage = await this.crowdsale.partnersPercentage();
const total = tokenSalePercentage.toNumber() + foundersPercentage.toNumber() + foundationPercentage.toNumber() + partnersPercentage.toNumber()
total.should.equal(100);
});
});
});
YAY!!! π That's the completed code for the crowdsale smart contract. Now let's deploy it to a live network so that you can start raising funds in your real world ICO!
13. Deployment
In order to deploy the smart contract, we must first create a deployment script. We can do that like this:
$ touch migrations/2_deploy_crowdsale.js
We can fill out the migration script like this:
const DappToken = artifacts.require("./DappToken.sol");
const DappTokenCrowdsale = artifacts.require("./DappTokenCrowdsale.sol");
const ether = (n) => new web3.BigNumber(web3.toWei(n, 'ether'));
const duration = {
seconds: function (val) { return val; },
minutes: function (val) { return val * this.seconds(60); },
hours: function (val) { return val * this.minutes(60); },
days: function (val) { return val * this.hours(24); },
weeks: function (val) { return val * this.days(7); },
years: function (val) { return val * this.days(365); },
};
module.exports = async function(deployer, network, accounts) {
const _name = "Dapp Token";
const _symbol = "DAPP";
const _decimals = 18;
await deployer.deploy(DappToken, _name, _symbol, _decimals);
const deployedToken = await DappToken.deployed();
const latestTime = (new Date).getTime();
const _rate = 500;
const _wallet = accounts[0]; // TODO: Replace me
const _token = deployedToken.address;
const _openingTime = latestTime + duration.minutes(1);
const _closingTime = _openingTime + duration.weeks(1);
const _cap = ether(100);
const _goal = ether(50);
const _foundersFund = accounts[0]; // TODO: Replace me
const _foundationFund = accounts[0]; // TODO: Replace me
const _partnersFund = accounts[0]; // TODO: Replace me
const _releaseTime = _closingTime + duration.days(1);
await deployer.deploy(
DappTokenCrowdsale,
_rate,
_wallet,
_token,
_cap,
_openingTime,
_closingTime,
_goal,
_foundersFund,
_foundationFund,
_partnersFund,
_releaseTime
);
return true;
};
Now we can run the migration script and deploy the crowdsale like this:
$ truffle migrate --network ropsten
Congratulations! π You have successfully built an ERC-20 token and crowd sale smart contract on Ethereum! You can download the full source code to this tutorial from github here.
Want to Hire an Expert?
If you're interested in hiring an ICO expert, I can lead your project step-by-step from "zero to ICO" with my battle tested ICO solution!
Learn MoreHappy with this tutorial? Then you NEED to join my free training here where I'll show you how to build a real world blockchain app so that you can become a highly paid blockchain developer!