How To Build A Simple Capped Crowdsale Token Using OpenZeppelin Library — Part 2

Using Open-Zeppelin Library to build a basic capped ERC20 crowdsale.

In Part 1, we built a capped ExampleToken. In this article, we will dive into crowdsale contract ExampleTokenCrowdsale and understand it’s inner workings.

Lets first look at our imported contracts from the open zeppelin-solidity library.

Crowdsale.sol

This contract implements the core crowdsale functionality. There are a few global variables.

  • ERC20 token— As you can see, this token is inheriting ERC20.sol. This variable will contain the address of our ExampleToken contract.
  • wallet — This will store address where ether contributions will get collected.
  • rate — This will store how many token units a buyer gets per wei.
  • weiRaised- How many wei get raised via contribution.

This contract also defines a constructor which is getting used by our ExampleTokenCrowsale.sol. The most important method of this contract is buytokens(_beneficiary). Most of the functions in this contract are getting called by this method, it performs these tasks:

  • Pre-validation, if we have any condition
  • Process purchase (Transferring token)
  • Update different states (ex- wei increased)
  • Post validation if required
pragma solidity ^0.4.24;
import "../token/ERC20/ERC20.sol";import "../math/SafeMath.sol";import "../token/ERC20/SafeERC20.sol";
contract Crowdsale {using SafeMath for uint256;using SafeERC20 for ERC20;
ERC20 public token;
address public wallet;
uint256 public rate;
uint256 public weiRaised;
event TokenPurchase(address indexed purchaser,address indexed beneficiary,uint256 value,uint256 amount);
constructor(uint256 _rate, address _wallet, ERC20 _token) public {require(_rate > 0);require(_wallet != address(0));require(_token != address(0));
rate = _rate;wallet = _wallet;token = _token;}
function () external payable {buyTokens(msg.sender);}
function buyTokens(address _beneficiary) public payable {
uint256 weiAmount = msg.value;_preValidatePurchase(_beneficiary, weiAmount);
uint256 tokens = _getTokenAmount(weiAmount);
weiRaised = weiRaised.add(weiAmount);
_processPurchase(_beneficiary, tokens);emit TokenPurchase(msg.sender,_beneficiary,weiAmount,tokens);
_updatePurchasingState(_beneficiary, weiAmount);
_forwardFunds();_postValidatePurchase(_beneficiary, weiAmount);}
function _preValidatePurchase(address _beneficiary,uint256 _weiAmount)internal{require(_beneficiary != address(0));require(_weiAmount != 0);}
function _postValidatePurchase(address _beneficiary,uint256 _weiAmount)internal{// optional override}
function _deliverTokens(address _beneficiary,uint256 _tokenAmount)internal{token.safeTransfer(_beneficiary, _tokenAmount);}
function _processPurchase(address _beneficiary,uint256 _tokenAmount)internal{_deliverTokens(_beneficiary, _tokenAmount);}
function _updatePurchasingState(address _beneficiary,uint256 _weiAmount)internal{// optional override}
function _getTokenAmount(uint256 _weiAmount)internal view returns (uint256){return _weiAmount.mul(rate);}
function _forwardFunds() internal {wallet.transfer(msg.value);}}

MintedCrowdsale.sol

This contract gets called through crowdsale.sol contract and will call mintableToken’s mint() function to manage token minting as we have discussed in last tutorials.

pragma solidity ^0.4.24;
import "../Crowdsale.sol";import "../../token/ERC20/MintableToken.sol";
contract MintedCrowdsale is Crowdsale {
function _deliverTokens(address _beneficiary,uint256 _tokenAmount)internal{require(MintableToken(address(token)).mint(_beneficiary, _tokenAmount));}}

CappedCrowdsale.sol

This contract is responsible for managing capping functionality and we will extend this contract to put the cap on investors contributions. We will also define the maximum total ether contribution cap using this contract.

pragma solidity ^0.4.24;
import "../../math/SafeMath.sol";import "../Crowdsale.sol";
contract CappedCrowdsale is Crowdsale {using SafeMath for uint256;
uint256 public cap;
constructor(uint256 _cap) public {require(_cap > 0);cap = _cap;}
function capReached() public view returns (bool) {return weiRaised >= cap;}
function _preValidatePurchase(address _beneficiary,uint256 _weiAmount)internal{super._preValidatePurchase(_beneficiary, _weiAmount);require(weiRaised.add(_weiAmount) <= cap);}}

Capping investors contribution

Now let’s write our logic for capping investors contribution, We will accept minimum 2 ether and maximum 50 ether from an investor.

uint256 public investorMinCap = 2000000000000000000;uint256 public investorHardCap = 50000000000000000000;
mapping(address => uint256) public contributions;
constructor(uint256 _rate,address _wallet,ERC20 _token,uint256 _cap)Crowdsale(_rate, _wallet, _token)CappedCrowdsale(_cap)public{}
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 have defined investorMinCap (2 ether in wei) and investorMaxCap(50 ether in wei), We also defined mapping contributions to track investors contribution. We are calling super at the first line, so if any contract will inherit this contract, will be able to execute it’s _preValidatePurchase() method first. Then we are checking the condition for capping investors and at the end updating contribution in our mapping.

require(_newContribution >= investorMinCap && _newContribution <= investorHardCap);

This way we have now put a cap on investors contribution.

Capping Total ether raised

Now let’s look at ExampleTokenCrowdsale constructor. Here we are setting 4 parameters.

  • _rate
  • _wallet
  • _token
  • _cap — This will define cap on ether contribution.

We will set these methods while deploying the contract.

constructor (uint256 _rate,address _wallet,ERC20 _token,uint256 _cap)Crowdsale(_rate, _wallet, _token)CappedCrowdsale(_cap)public{}

That’s it, We successfully created a capped crowdsale contract. Now, let’s test it, we have discussed testing smart contract in our last series of solidity Curd app.

Testing Crowdsale Contract

Let’s run some commands to test our smart contract.

truffle compiletruffle developmigrate --reset

Let’s deploy our ExampleToken contract, Look we are passing our token name, symbol and decimal point for our smart contract.

ExampleToken.deployed("Example Token", "EXM", 18).then((t) => {token = t;})

Now deploy our ExampleTokenCrowdsale contract. We are passing our 4 parameters here rate(500 token / ether), wallet( web3.eth.accounts[0]) , ERC20 token(Example Token address) and cap (200 ether).

ExampleTokenCrowdsale.deployed(500, web3.eth.accounts[0], token.address , new web3.BigNumber(web3.toWei(200, 'ether'))).then((t) => {sale = t;})

Before buying tokens from our crowdsale contract, we need to perform a small step, We need to transfer ownership of token contract to crowdsale contract so it can mint tokens. Check MintableToken.sol and you will understand why we are performing this step.

token.transferOwnership(sale.address)

Now let check our investor cap constraint, We will perform 4 transactions.

  1. As our minimum contribution cap is 2 ether. First, we will try to buy tokens with 1 ether. It should fail.
sale.buyTokens(web3.eth.accounts[1], {value : new web3.BigNumber(web3.toWei(1, 'ether')) , from : web3.eth.accounts[1]});

2. Now we will buy tokens with 2 ether, it should succeed.

sale.buyTokens(web3.eth.accounts[1], {value : new web3.BigNumber(web3.toWei(2, 'ether')) , from : web3.eth.accounts[1]});

3. Now we will buy tokens with 48 ether from the same investor account. It should pass

sale.buyTokens(web3.eth.accounts[1], {value : new web3.BigNumber(web3.toWei(48, 'ether')) , from : web3.eth.accounts[1]});

4. Now if we try to buy any more tokens from the same account it should fail.

sale.buyTokens(web3.eth.accounts[1], {value : new web3.BigNumber(web3.toWei(1, 'ether')) , from : web3.eth.accounts[1]});

You can check the total supply and balance of the account in the middle as you feel comfortable, you will be able to see that total supply is getting increased as we are buying tokens.

token.balanceOf(web3.eth.accounts[1]).then(result => result.toNumber())

In the same way, you can test the hard cap of our crowdsale by buying tokens from the different account. Let me know how it went. You don’t need to test open-zeppelin contracts as they are well tested already.

Conclusion

So we have created our capped token and crowdsale contract, We learned how to use open-zeppelin library and how we can use them to build an ERC20 token. We have also learned inner workings of different open-zeppelin library contracts.

You can also add additional constraints such as adding a time limit for sale and deciding when sale should start or end. Which we will cover in the next part of our tutorial series.

Notes & Suggestions

A small bug can turn the whole project upside down. Using libraries like Open-zeppelin helps here by providing some fully tested core functionality of an ERC20 token.

If you have questions about any part of the tutorial, feel free to ask questions on the comments section. Go through open-zeppelin’s library and let me know what you want to learn next.

Originally published:

October 23, 2018