ERC20 contract contains all the functionality necessary to buy and sell it using another contract (for example, an online exchange). An online exchange is a complex tool, mostly due to the need to implement "trading glass" - an algorithm for figuring out the current market price. Creating such a tool requires research and planning and definitely is not an everyday task.
However, there is a case when writing an online exchange becomes easy: just imagine that we KNOW what the price should be as we set it ourselves. This is what an ICO contract is for: to sell ERC20 tokens at a predefined price.
In this tutorial we will walk through a typical Solidity contract for an ICO Crowdsale, produced by an ICO Generator tool.
When an ICO Generator creates a script, it adds few contracts to it. All of them, except for the ICO contract itself and a helper contract called ShareHolder, were described in the previous section when we discussed an ERC20 contract. Below, these contracts are listed as outlines, with "..." instead of the code.
// --- - Safe Math library SafeMath { ... // discussed in a prev. chapter } // --- - ERC20 Interface contract ERC20Interface { ... } // --- - ERC20 Token contract ERC20Token is ERC20Interface { ... } // --- - Ownable contract Ownable { ... } // --- - Mintable contract MintableToken is ERC20Token, Ownable { ... } // --- - My Token contract MyToken is MintableToken { ... }
ShareHolder is not a mandatory part of ICO Crowdsale contract, and can be ommitted.
ShareHolder contract is described in a ShareHolder Users Guide. The contract itself can be accessed using our Web Site as ShareHolder Contract.
As the tutorial provides detailed description of the ShareHolder contract, here we are only touching it briefly.
You can instruct other contracts to send part of their profit to ShareHolder contract, and it will keep that profit as it grows. "Shares" of shareHolder can be purchased and sold, they work as cumulative bank accounts, which means their price can only increase. So this is just a tool to distribute profit among share holders.
In ICO Generator, you can choose to include ShareHolder support to your ICO and distribute shares to each token buyer, like an extra bonus. Once again, this is not mandatory.
// --- ShareHolder forward declaration --- contract ShareHolder { function distributeBonusShares(address, uint, string) public; function undistributeBonusShares(string) public; } // --- End of ShareHolder forward declaration ---
This contract manages the crowd sale, keeps track of tokens and so on. As was mentioned above, it is a simplified online exchange.
We are going to walk through the contract, providing necessary explanations to the code. But before doing it, we need to understand better the options we have.
You have already seen one option: should ShareHolder contract be included in our Crowd Sale contract. Here are few other options (ICO Generator will create code with different options, depending on options you checked in a form describing the contract you want to generate. Same code can be written manually, of course):
Decimals. This is for formatting purposes only: how many digits should be
displayed for us, humans. Should it be 1 token or 1.23432456 tokens?
Token in the same file or in a different one? When creating ICO Crowd Sale contract,
we have two choices. We can place the token in the same file where the Crowd Sale contract is,
or in a different file. Having token in the same file makes it easy to call its functions,
but why should we have a Crowd Sale contract that lasts only about one month together with
a Token, that is intended to be used for decades?
Also, if a CrowdSale contract is allowed to accept different tokens, we'll be able, in theory,
to reuse the Crowd Sale Contract for more than one ICO (though no one does it).
Let's compare the two cases:
As you can see, the difference is in the way we call the Token (by name or by address) and
access it (accessing with address requires forward declaration).
These features are relatively simple and will be explained in details leter when
we walk through the code. Here we are only going to explain what they do.
Can we pause the Crowd Sale?
A valuable feature in case something not ready and you learn it when it is too late.
This feature blocks the sale of tokens until pause is over. Note that it works well with
the next feature called "Can extend Crowdsale time".
Can we extend the Crowdsale duration?"
As we mentioned already, this feature is very handy when the crowd sale was paused and
some time was lost. Also, sometimes things go too slow and extra time can save the
campaign even if there was no pause in Crowd Sale.
Has Bonus Tokens. This feature allows you to distribute up to apredefined
number of tokens to a beneficiary. For example, you may want to reward the developers team,
or participants of your Bounty Campaign.
Use Whitelist. Whitelisting is used if for some reasons you want to sell
tokens to qualified buyers only. Usually it comes together with KYC (Know Your Customer)
data collection. In our code it is done by assigning a "whitelister", an address that can
call function to "whitelist" people. Whitelisting itself works by setting/removing
limits, max. number of ether one can spend.
Let's walk through the code of an ICO contract, providing the necessary explanations.
The contract we choose includes all the features and it has a Token contract in a
separate file.
ShareHolder forward declaration: as we use ShareHolder contract and it is located
in a different file (technically, it does not have to be YOUR contract, you can
interact with any contract in a block chain) we need to tell our contract what
functions to expect. We only need to list functions that we are going to call.
We have changed the name of a Token from MyToken to MySuperToken, just to prove
that's possible. As with ShareHolder, the Token is located in a different file, so
we need to provide a forward declaration of all functions we are going to use from
our ICO contract:
We have changed the name of an ICO contract to MySuper ICO, just to prove
it is possible. This is a main contract of our crowd sale, and it is
derived from Ownable.
Tokens can be sold or distributed as a reward (for example, among a team
of developers and testers). There should be a max. number of tokets
that the contract is allowed to distribute:
The following code is part of our support for whitelisting.
First of all, we have created mapping called m_mapLevels.
It contains a list of "levels", max. amounts people are allowed to
spend on tokens. This mapping is filled in a constructor:
It looks like in the list above, there are two kinds of people:
first "level" can spend up to 1 ether, the second level is pretty much
unlimited (1 tether is too much to be considered as a realistic amount).
If you use ICO Generator tool, you can alter the list on "Levels" tab,
by editing a structure that will be used to alter the ICO contract's code when you press
the "Update Levels" button. Of course you can also do it manually by
editing the code in a constructor.
As we have decided to include ShareHolder support in our ICO, we have to provide
the contract address. Also we need to know how many shares tokens should qualify
to get one share (m_nNumOfTokensPerShare) and what is the name of our ICO campaign
from ShareHolder's point of view.
The last parameter is required because ShareHolder contract can, in theory, receive
payments from multiple sources. It keeps records of all transactions of course,
so the m_strCampaignName is used for book keeping purposes. When you look through
ShareHolder's stats, you will be able to find out who made a deposit.
Variables holding flagsof contract's state are self-explanatory.
A Crowd Sale campaign is usually divided to stages. For example, people
buying tokens during the first week of a campaign may get a signifficant discount.
Also, it is a good idea to store sale statistics in the same place (i.e.
attached to dates interval):
Same as with whitelisting levels, crowdsale stages are filled in a constructor. And
same as whitelisting levels, crowdsale stages can be altered in ICO Generator
in the "Prices" tab:
Crowd Sale usually sets goals: a minimum goal is an amount required for crowd sale
to be considered successful (and if not, return money) and a max. goal - an amount
when people say "no, thanks". As saying "no, thanks" to money is not easy, the usual
approach is to set low min. goal (let's make sure we get something) and max. goal
is set unreallistically high (100,000,000,000 ether and we say "enough"). But of
course, there are exceptions.
If ICO is successful, funds are transfered to beneficiaries. There can be more than one
receiver, for example, owner, developers (note: this is another way of rewarding developers,
in addition to giving them free tokens) etc.
Wallets are assigned in a constructor and can be edited in ICO Generator same way
we edited prices and levels:
Another self-explanatory parameter:
Events in Solidity are used for logging purpose. In other words, you can write
information to block chain using events, but you can not access this information
from your contract. However this information is accessible from Web site
that has an access to block chain, and it is much cheaper to store data in event
logs, then in contract's parameters.
So we are going to write to event logs whatever we want to access later on from
our web site: contract's statistics.
As mentioned above, in constructor we fill few structures that are
not supposed to be changed in future (like list of beneficiaries).
We also create cobjects of external contracts (Token and ShareHolder).
An "empty" function. This function has no name and is used
(according to Solidity rules) an an "intercepting" function for
unexpected things. For example, if someone sends money to a contract
(not function of a contract that is supposed to receive money, but to
an address of a contract), this function catches it.
An obvious use: to call buyTokens function from it.
Note that this is not just a "nice to have" thing, but an improvement
that can get you some extra users. For example, say your ICO campaign does not
have a web site that works in a tandem with MetaMask. Then in order to
buy tokens, a client has to run a wallet that can work with contract's functions
(MEW, Mist) which is not nearly as easy as working with MetaMask.
By making it possible to send money to contract (an address, without a function), you
make sure those people will not abandon you.
Of course to SELL their tokens they will have to learn using advanced tools :)
The following function can be used by contract owner only
(note the "onlyOwner" modifier) to assign free tokens to a selected address.
Token purchase function. Note similarities between this function and
distributeTokens above. Technically, the two functions could be combined.
Distribute money upon successful end of campaign. Note that
this function uses cycle. It means that the list of wallets
MUST be short, otherwise we can run out of gas.
As a campaign is over, we need to figure out if it was successful or not.
The following function cycles through the list of
campaign "stages", checking current date against stage's date in order
to figure out what stage is it now.
The following function is used to alter the duration of a specified stage of a campaign.
Note that dates of later stages are adjusted automatically.
Set the "paused" flag to temporary suspend a crowd sale.
The following function is called if the crowdsale campaign failed to
reach its goal.
Note that we use "send on demand" pattern here, so in order to get money, the
client has to demand it (by calling this function).
Getters: static functions to return contract's state(s).
Most functions are self-explanatoty.
Whitelisting functions. Note that if the whitelisting is not used, these functions are
not required.
Should we keep contracts together?
Same files
Different files
interface MyToken
{
// add any functions you call from ICO
function finishMinting() public returns (bool);
function mint(address _to, uint256 _amount)
public returns (bool);
function totalSupply() constant
public returns (uint256);
function transfer(address receiver,
uint amount) public;
function balanceOf( address _address )
public returns(uint256);
}
contract ICO is Ownable
{
...
MyToken public m_token;
contract ICO is Ownable
{
...
MyToken public m_token;
address m_addrTokenAddress = 0x00;
function ICO() public
{
...
m_token = new MyToken();
...
}
...
function ICO() public
{
...
m_token = MyToken(m_addrTokenAddress);
...
}
...
function getTokenAddress()
constant public returns (address)
{
return address(m_token);
}
Additional options available for ICO contract
Walking through the code
contract ShareHolder
{
function distributeBonusShares(address, uint, string) public;
function undistributeBonusShares(string) public;
}
interface MySuperToken
{
// add any functions you call from ICO
function finishMinting() public returns (bool);
function mint(address _to, uint256 _amount) public returns (bool);
function totalSupply() constant public returns (uint256);
function transfer(address receiver, uint amount) public;
function balanceOf( address _address ) public returns(uint256);
}
contract MySuperICO is Ownable
{
using SafeMath for uint256;
MySuperToken public m_token;
address m_addrTokenAddress = 0x009876;
uint256 m_nMaxNumOfTokensAllowedToDistribute = 999;
mapping (string => uint256) public m_mapLevels; // For unlim use '1 tether'
mapping (address => string) public m_mapWhitelist; // addr =>"Gold", "Silver"...
address public m_addrWhiteLister = 0x6543;
... Constructor, see below ...
m_mapLevels[0] = '1 ether';
m_mapLevels[1] = '1 tether';
address m_addrShareHolder = address(0); // addr. of ShareHolder Profit Manager
ShareHolder m_contractShareHolder;
uint m_nNumOfTokensPerShare = 1; // How many tokens should you buy to get one share
string m_strCampaignName = "MySuperICO";
mapping(address => uint256) public m_mapBalanceOf;
bool m_bPaused = false;
bool public m_bRefunding = false; // Crowdsale ended, refund in progress
uint256 public m_nWeiRefunded; // The total number of wei refunded.
// Crowd sale stages and goals
struct SaleOptions
{
// Something like "WAIT_PRESALE", "PRESALE", "WAIT_SALE", "SALE_1", "SALE_2", "SALE_3", "WAIT_REFUND"
// The last one (REFUND) should not be added - it is assumed that last one is "REFUND".
// Also note that Crowdsale starts IMMEDIATELY. If you want a delay, add first item as "WAIT_PRESALE"
// and set its m_bTradingAllowed to false.
string status;
uint256 timeEnd;
uint256 tokenPrice;
uint256 nWeiRaised;
uint256 nTokensSold;
bool tradingAllowed;
}
SaleOptions[] m_saleOptions;
uint m_nCurrentStatusIdx = 0; // Index of array element in m_saleOptions
uint256 m_nTotalAmountRaised = 0;
// If not reached, issue a refund
uint256 m_nMinGoal = 100 ether;
// If reached, force finish of a campaign. If 0, no upper cup.
uint256 m_nMaxGoal = 2000 ether;
// --- Receiving wallets (owner, dev. team etc.)
// Wallet to receive the contract's balance once the sale
// finishes and the minimum goal is met.
struct Wallet
{
string strWalletName; // "Owner", "Dev. Team"...
address addrWallet; // Address to transfer to
uint nTokens; // How many tokens to transfer (or 0)
uint nPercentOfTokens; // Percent of issued tokens to transfer (100000 based) (or 0)
uint nWei; // Money to transfer (or 0)
uint nPercentWei; // Percent of raised to transfer (100000 based) (or 0)
}
Wallet[] m_arrReceivingWallets;
bool public m_bFinished = false;
Events
// address indexed _address, string strLevel
// strLevel == "Gold" or strLevel == "Removed"
event WhiteListed(address indexed, string);
event WhitelisterSet(address); // address addrWhiteLister
event Pause();
event Unpause();
event Withdrawal(address addrBeneficiary, uint256 nTokens,
uint256 nPercentOfTokens, uint256 nWei, uint256 nPercentWei);
event Extended(uint256 nNumOfDays, uint256 nStage);
event Finalized();
event Refunding();
event Refunded(address indexed beneficiary, uint256 weiAmount);
event eventTokenCreated(address indexed purchaser,
address indexed beneficiary, uint256 value, string strDistributionReason);
Functions
function ICO() public
{
m_saleOptions.push(SaleOptions("WAIT_PRESALE",1543266000,1000000000000000,0,0,false));,
m_saleOptions.push(SaleOptions("PRESALE",1545858000,1000000000000000,0,0,true));,
m_saleOptions.push(SaleOptions("WAIT_SALE",1548536400,1000000000000000,0,0,false));,
m_saleOptions.push(SaleOptions("SALE",1551214800,1000000000000000,0,0,true));,
m_saleOptions.push(SaleOptions("FINALIZING",1553634000,1000000000000000,0,0,false));
// ---
m_arrReceivingWallets.push(
Wallet("Owner", // string strWalletName;
0x949d4bC47fA7103cB3556852150bc580FA4499B9, // address addrWallet;
0, // uint nTokens; How many tokens to transfer (or 0)
0, // uint nPercentOfTokens; Percent of issued tokens to transfer (100000 based) (or 0)
0, // uint nWei; Money to transfer (or 0)
100000)); // uint nPercentWei; Percent of raised to transfer (100000 based) (or 0)
// ---
m_mapLevels[0] = '1 ether';
m_mapLevels[1] = '1 tether';
// ---
m_token = MySuperToken(m_addrTokenAddress);
if(m_addrShareHolder != address(0))
m_contractShareHolder = ShareHolder(m_addrShareHolder);
}
// fallback function can be used to buy tokens
function() public payable
{
require(!m_bPaused);
buyTokens(msg.sender);
}
// Distribute tokens to selected party. Tokens are assigned free,
// and if ICO fails, can not be refunded (as no one paid for them)
function distributeTokens(address beneficiary, uint256 nNumOfTokens,
string strDistributionReason) public onlyOwner
{
// First, it makes sure a campaign isn't over yet.
require (!m_bFinished);
// Then it figures out what is the current stage of a campaign
getCurrentStatusIdx(); // Init the member variable
// Few more checks to make sure request is legit
require(isTradingAllowed());
require (now < m_saleOptions[m_saleOptions.length - 1].timeEnd);
require(beneficiary != 0x0);
require(nNumOfTokens <= m_nMaxNumOfTokensAllowedToDistribute);
// How much tokens shall we have if we do it?
uint256 resultingTotalSupply = m_token.totalSupply().add(nNumOfTokens);
// return money if total allowed supply exceeded
require(m_nMaxGoal == 0 || m_nMaxGoal >= resultingTotalSupply);
// If everything was ok, mint the requested tokens
m_token.mint(beneficiary, nNumOfTokens);
// If we use ShareHolder, issue shares to accompany tokens
if(m_addrShareHolder != address(0))
{
uint nShares = nNumOfTokens / m_nNumOfTokensPerShare;
if(nShares > 0)
m_contractShareHolder.distributeBonusShares(beneficiary, nShares, m_strCampaignName);
}
// Finally, log the info about this event
eventTokenCreated(msg.sender, beneficiary, nNumOfTokens, strDistributionReason);
}
function buyTokens(address beneficiary) public payable
{
require (!m_bFinished && !m_bPaused
&& getWhiteListMaxAmount(beneficiary) > msg.value);
getCurrentStatusIdx(); // Init the member variable
require(isTradingAllowed());
require (m_saleOptions[m_nCurrentStatusIdx].tradingAllowed == true);
require (now < m_saleOptions[m_saleOptions.length - 1].timeEnd);
require(beneficiary != 0x0);
uint256 nTokenPrice = getTokenPrice();
require (msg.value >= nTokenPrice);
uint256 nNumOfTokens = msg.value.div(nTokenPrice);
uint256 resultingTotalSupply = m_token.totalSupply().add(nNumOfTokens);
// return money if total allowed supply exceeded
require(m_nMaxGoal == 0 || m_nMaxGoal >= resultingTotalSupply);
// update state
m_saleOptions[m_nCurrentStatusIdx].nWeiRaised =
m_saleOptions[m_nCurrentStatusIdx].nWeiRaised.add(msg.value);
m_nTotalAmountRaised = m_nTotalAmountRaised.add(msg.value);
m_token.mint(beneficiary, nNumOfTokens);
m_mapBalanceOf[msg.sender] += msg.value;
if(m_addrShareHolder != address(0))
{
uint nShares = nNumOfTokens / m_nNumOfTokensPerShare;
if(nShares > 0)
m_contractShareHolder.distributeBonusShares(msg.sender, nShares, m_strCampaignName);
}
eventTokenCreated(msg.sender, beneficiary, nNumOfTokens, "Bought");
}
function withdraw() onlyOwner public
{
require(goalReached() && m_bFinished &&
now < m_saleOptions[m_saleOptions.length - 2].timeEnd);
uint256 weiAmount = this.balance;
if(weiAmount > 0)
{
for(uint i = 0; i < m_arrReceivingWallets.length; i++)
{
// Add check for sufficient funds
if(m_arrReceivingWallets[i].nTokens > 0)
m_token.mint(m_arrReceivingWallets[i].addrWallet,
m_arrReceivingWallets[i].nTokens);
if(m_arrReceivingWallets[i].nPercentOfTokens > 0)
m_token.mint(m_arrReceivingWallets[i].addrWallet,
m_token.totalSupply().mul(m_arrReceivingWallets[i].
nPercentOfTokens).div(100000));
if(m_arrReceivingWallets[i].nWei > 0 &&
m_arrReceivingWallets[i].nWei <= weiAmount)
m_arrReceivingWallets[i].addrWallet.transfer(
m_arrReceivingWallets[i].nWei);
if(m_arrReceivingWallets[i].nPercentWei > 0)
m_arrReceivingWallets[i].addrWallet.transfer(
weiAmount.mul(m_arrReceivingWallets[i].nPercentWei).
div(100000));
// ---
Withdrawal(m_arrReceivingWallets[i].addrWallet,
m_arrReceivingWallets[i].nTokens,
m_arrReceivingWallets[i].nPercentOfTokens,
m_arrReceivingWallets[i].nWei,
m_arrReceivingWallets[i].nPercentWei);
}
}
}
function finish() onlyOwner public
{
require(!m_bFinished);
require(now < m_saleOptions[m_saleOptions.length - 2].timeEnd);
m_bFinished = true;
m_token.finishMinting();
if(goalReached())
withdraw();
else
{
m_bRefunding = true;
Refunding();
}
Finalized();
}
function getCurrentStatusIdx() internal returns (uint256 nIdx)
{
for(uint i = m_saleOptions.length - 1; i >= 0; i--)
{
if(now < m_saleOptions[i].timeEnd)
{
m_nCurrentStatusIdx = i;
return i;
}
}
// Something went terribly wrong!
m_nCurrentStatusIdx = m_saleOptions.length - 1;
return m_saleOptions.length - 1;
}
function extendTimeEnd(uint256 nNumOfDays, uint256 nStage)
onlyOwner public
{
require(!m_bFinished);
require(nStage < m_saleOptions.length);
require(now + 1 days <= m_saleOptions[nStage].timeEnd);
require(nNumOfDays > 0);
m_saleOptions[nStage].timeEnd += nNumOfDays * 24 * 3600;
for(uint i = nStage + 1; i < m_saleOptions.length; i++)
m_saleOptions[i].timeEnd += nNumOfDays * 24 * 3600;
Extended(nNumOfDays, nStage);
}
function pause() onlyOwner public
{
require(!m_bPaused);
m_bPaused = true;
Pause();
}
function unpause() onlyOwner public
{
// To extend current period to compensate for pause,
// the owner can call extendTimeEnd()
require(m_bPaused);
m_bPaused = false;
Unpause();
}
function refund(address _investor) public
{
require(m_bFinished);
require(m_bRefunding);
require(m_mapBalanceOf[_investor] > 0);
uint256 weiAmount = m_mapBalanceOf[_investor];
m_mapBalanceOf[_investor] = 0;
m_nWeiRefunded = m_nWeiRefunded.add(weiAmount);
Refunded(_investor, weiAmount);
if(m_addrShareHolder != address(0))
{
m_contractShareHolder.undistributeBonusShares(m_strCampaignName);
}
_investor.transfer(weiAmount);
}
function getShareHolder() public constant
returns (address) { return m_addrShareHolder; }
// ------
function getTokenAddress() constant public
returns (address)
{
return address(m_token);
}
// ------
function getCurrentStatus() public constant
returns (string)
{
return m_saleOptions[m_nCurrentStatusIdx].status;
}
// ------
//determine the price of the token depending on ICO stage
function getTokenPrice() public constant
returns (uint256 nTokenPrice)
{
return m_saleOptions[m_nCurrentStatusIdx].tokenPrice;
}
// ------
function isTradingAllowed() public constant
returns (bool)
{
return m_saleOptions[m_nCurrentStatusIdx].tradingAllowed;
}
// ------
function hasEnded() public constant
returns (bool)
{
return (now > m_saleOptions[
m_saleOptions.length - 1].timeEnd);
}
// ------
function goalReached() public constant returns (bool)
{
return m_nTotalAmountRaised >= m_nMinGoal;
}
modifier onlyOwnerOrWhiteLister()
{
require((msg.sender == m_addrOwner) ||
(msg.sender == m_addrWhiteLister));
_;
}
// ------
function whiteListUser(address addrUser, string strLevel)
public onlyOwnerOrWhiteLister
{
m_mapWhitelist[addrUser] = strLevel;
}
// ------
function whiteListUsers(address[] users)
public onlyOwnerOrWhiteLister
{
for(uint i = 0; i < users.length; i++)
m_mapWhitelist[users[i]] = strLevel;
}
// ------
function unWhiteListUser(address addrUser) public onlyOwnerOrWhiteLister
{
delete m_mapWhitelist[addrUser];
}
// ------
function unWhiteListUsers(address[] users) public onlyOwnerOrWhiteLister
{
for (uint i = 0; i < users.length; i++) {
delete m_mapWhitelist[users[i]];
}
// ------
function getWhiteListMaxAmount(address addUser)
public constant returns (uint256)
{
if(m_mapWhitelist[addUser])
return m_mapLevels[m_mapWhitelist[addUser]];
else return 0;
}
// ------
function getWhiteListStatus(address addUser) public
constant returns (string)
{
if(m_mapWhitelist[addUser])
return m_mapWhitelist[addUser];
return "";
}
// ------
function setWhiteLister(address addrWhiteLister)
public onlyOwnerOrWhiteLister
{
require(_newWhiteLister != address(0));
WhitelisterSet(addrWhiteLister);
m_addrWhiteLister = addrWhiteLister;
}