Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Ability to use crowdsale contract with pre minted tokens #554

Closed
wants to merge 11 commits into from
11 changes: 6 additions & 5 deletions contracts/crowdsale/Crowdsale.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import "../math/SafeMath.sol";
* Crowdsales have a start and end timestamps, where investors can make
* token purchases and the crowdsale will assign them tokens based
* on a token per ETH rate. Funds collected are forwarded to a wallet
* as they arrive. The contract requires a MintableToken that will be
* minted as contributions arrive, note that the crowdsale contract
* as they arrive. The contract requires a token contract which inherits
* the Mintable interface which provides the mint function that will be
* called as contributions arrive, note that the crowdsale contract
* must be owner of the token in order to be able to mint it.
*/
contract Crowdsale {
using SafeMath for uint256;

// The token being sold
MintableToken public token;
// minter interface providing the tokens being sold
Mintable public token;

// start and end timestamps where investments are allowed (both inclusive)
uint256 public startTime;
Expand All @@ -43,7 +44,7 @@ contract Crowdsale {
event TokenPurchase(address indexed purchaser, address indexed beneficiary, uint256 value, uint256 amount);


function Crowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, address _wallet, MintableToken _token) public {
function Crowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, address _wallet, Mintable _token) public {
require(_startTime >= now);
require(_endTime >= _startTime);
require(_rate > 0);
Expand Down
61 changes: 61 additions & 0 deletions contracts/examples/PreMintedCrowdsale.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
pragma solidity ^0.4.18;

import "../token/ERC20/PseudoMinter.sol";
import "../crowdsale/Crowdsale.sol";
import "./SimpleToken.sol";

/**
* @title PreMintedCrowdsaleVault
* @dev Simple contract which acts as a vault for the pre minted tokens to be
* sold during crowdsale. The tokens approve function is limited to one call
* which makes the PreMintedCrowdsale to a capped crowdsale.
*/
contract PreMintedCrowdsaleVault {

SimpleToken public token;
PreMintedCrowdsale public crowdsale;

function PreMintedCrowdsaleVault(
uint256 _startTime,
uint256 _endTime,
uint256 _rate,
address _wallet
)
public
{
token = new SimpleToken();
PseudoMinter _pseudoMinter = new PseudoMinter(token, this);

crowdsale = new PreMintedCrowdsale(_startTime, _endTime, _rate, _wallet, _pseudoMinter);
_pseudoMinter.transferOwnership(crowdsale);

token.approve(_pseudoMinter, token.balanceOf(this));
}
}

/**
* @title PreMintedCrowdsale
* @dev This is an example of a crowdsale which has had its tokens already minted
* in advance. By storing the spendable tokens in the PreMintedCrowdsaleVault
* and limiting the call to the tokens approve function, the crowdsale supports a
* definite hard cap.
*/
contract PreMintedCrowdsale is Crowdsale {

function PreMintedCrowdsale(
uint256 _startTime,
uint256 _endTime,
uint256 _rate,
address _wallet,
Mintable _token
)
public Crowdsale(_startTime, _endTime, _rate, _wallet, _token)
{
}

// @return true if crowdsale event has ended
function hasEnded() public view returns (bool) {
bool capReached = PseudoMinter(token).availableSupply() < rate;
return super.hasEnded() || capReached;
}
}
2 changes: 1 addition & 1 deletion contracts/examples/SampleCrowdsale.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ contract SampleCrowdsaleToken is MintableToken {
*/
contract SampleCrowdsale is CappedCrowdsale, RefundableCrowdsale {

function SampleCrowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, uint256 _goal, uint256 _cap, address _wallet, MintableToken _token) public
function SampleCrowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, uint256 _goal, uint256 _cap, address _wallet, Mintable _token) public
CappedCrowdsale(_cap)
FinalizableCrowdsale()
RefundableCrowdsale(_goal)
Expand Down
2 changes: 1 addition & 1 deletion contracts/mocks/CappedCrowdsaleImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract CappedCrowdsaleImpl is CappedCrowdsale {
uint256 _rate,
address _wallet,
uint256 _cap,
MintableToken _token
Mintable _token
) public
Crowdsale(_startTime, _endTime, _rate, _wallet, _token)
CappedCrowdsale(_cap)
Expand Down
2 changes: 1 addition & 1 deletion contracts/mocks/FinalizableCrowdsaleImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ contract FinalizableCrowdsaleImpl is FinalizableCrowdsale {
uint256 _endTime,
uint256 _rate,
address _wallet,
MintableToken _token
Mintable _token
) public
Crowdsale(_startTime, _endTime, _rate, _wallet, _token)
{
Expand Down
2 changes: 1 addition & 1 deletion contracts/mocks/RefundableCrowdsaleImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract RefundableCrowdsaleImpl is RefundableCrowdsale {
uint256 _rate,
address _wallet,
uint256 _goal,
MintableToken _token
Mintable _token
) public
Crowdsale(_startTime, _endTime, _rate, _wallet, _token)
RefundableCrowdsale(_goal)
Expand Down
10 changes: 10 additions & 0 deletions contracts/token/ERC20/Mintable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pragma solidity ^0.4.18;


/**
* @title Mintable
* @dev Interface for mintable token contracts
*/
contract Mintable {
function mint(address _to, uint256 _amount) public returns (bool);
}
3 changes: 2 additions & 1 deletion contracts/token/ERC20/MintableToken.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pragma solidity ^0.4.18;

import "./Mintable.sol";
import "./StandardToken.sol";
import "../../ownership/Ownable.sol";

Expand All @@ -10,7 +11,7 @@ import "../../ownership/Ownable.sol";
* @dev Issue: * https://github.com/OpenZeppelin/zeppelin-solidity/issues/120
* Based on code by TokenMarketNet: https://github.com/TokenMarketNet/ico/blob/master/contracts/MintableToken.sol
*/
contract MintableToken is StandardToken, Ownable {
contract MintableToken is Mintable, StandardToken, Ownable {
event Mint(address indexed to, uint256 amount);
event MintFinished();

Expand Down
61 changes: 61 additions & 0 deletions contracts/token/ERC20/PseudoMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
pragma solidity ^0.4.18;

import "./ERC20.sol";
import "./Mintable.sol";
import "../../math/SafeMath.sol";
import "../../ownership/Ownable.sol";

/**
* @title PseudoMinter
* @dev Proxy contract providing the necessary minting abbilities needed
* within crowdsale contracts to ERC20 token contracts with a pre minted
* fixed amount of tokens.
* PseudoMinter is initialized with the token contract and the vault contract
* which provides the spendable tokens. Cap is automatically set by approving
* to the PseudoMinter instance the chosen amount of tokens. Be aware that
* this does not necessarily represent the hard cap of spendable tokens. If
* vault can arbitrarily call the tokens approve function, this might even
* be a security risk since the cap can be manipulated at will. Best practice
* is to design vault as a contract with limited ablity to call the tokens
* approve function.
* For an example implementation see contracts/example/PreMintedCrowdsale.sol
*/
contract PseudoMinter is Mintable, Ownable {
using SafeMath for uint256;

// The token being sold
ERC20 public token;
// address which provides tokens via token.approve(...) function
address public vault;
// amount of tokens which have been pseudo minted
uint256 public tokensMinted;

function PseudoMinter(ERC20 _token, address _vault) public {
require(address(_token) != 0x0);
require(_vault != 0x0);

token = _token;
vault = _vault;
}

/**
* @dev Function to pseudo mint tokens, once the approved amount is used,
* cap is automatically reached.
* @param _to The address that will receive the minted tokens.
* @param _amount The amount of tokens to mint.
* @return A boolean that indicates if the operation was successful.
*/
function mint(address _to, uint256 _amount) onlyOwner public returns (bool) {
tokensMinted.add(_amount);
token.transferFrom(vault, _to, _amount);
return true;
}

/**
* @dev returns amount of tokens that can be pseudo minted. Be aware that
* this does not necessarily represent the hard cap of spendable tokens!
*/
function availableSupply() public view returns (uint256) {
return token.allowance(vault, this);
}
}
4 changes: 3 additions & 1 deletion test/crowdsale/CappedCrowdsale.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ contract('CappedCrowdsale', function ([_, wallet]) {

describe('creating a valid crowdsale', function () {
it('should fail with zero cap', async function () {
await CappedCrowdsale.new(this.startTime, this.endTime, rate, wallet, 0).should.be.rejectedWith(EVMRevert);
await CappedCrowdsale
.new(this.startTime, this.endTime, rate, wallet, 0, this.token.address)
.should.be.rejectedWith(EVMRevert);
});
});

Expand Down
2 changes: 1 addition & 1 deletion test/crowdsale/RefundableCrowdsale.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ contract('RefundableCrowdsale', function ([_, owner, wallet, investor]) {

describe('creating a valid crowdsale', function () {
it('should fail with zero goal', async function () {
await RefundableCrowdsale.new(this.startTime, this.endTime, rate, wallet, 0, { from: owner })
await RefundableCrowdsale.new(this.startTime, this.endTime, rate, wallet, 0, this.token.address, { from: owner })
.should.be.rejectedWith(EVMRevert);
});
});
Expand Down
81 changes: 81 additions & 0 deletions test/examples/PreMintedCrowdsale.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { advanceBlock } from '../helpers/advanceToBlock';
import { increaseTimeTo, duration } from '../helpers/increaseTime';
import latestTime from '../helpers/latestTime';
import EVMRevert from '../helpers/EVMRevert';

const BigNumber = web3.BigNumber;

require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();

const PreMintedCrowdsale = artifacts.require('PreMintedCrowdsale');
const PreMintedCrowdsaleVault = artifacts.require('PreMintedCrowdsaleVault');
const PseudoMinter = artifacts.require('./token/ERC20/PseudoMinter.sol');

contract('PreMintedCrowdsale', function ([_, wallet]) {
const rate = new BigNumber(1000);
before(async function () {
// Advance to the next block to correctly read time in the solidity "now" function interpreted by testrpc
await advanceBlock();
});

beforeEach(async function () {
this.startTime = latestTime() + duration.weeks(1);
this.endTime = this.startTime + duration.weeks(1);

this.vault = await PreMintedCrowdsaleVault.new(this.startTime, this.endTime, rate, wallet);
this.crowdsale = PreMintedCrowdsale.at(await this.vault.crowdsale());
this.pseudoMinter = PseudoMinter.at(await this.crowdsale.token());
this.tokenCap = new BigNumber(await this.pseudoMinter.availableSupply.call());
this.cap = this.tokenCap.div(rate);
this.lessThanCap = rate;
});

describe('accepting payments', function () {
beforeEach(async function () {
await increaseTimeTo(this.startTime + duration.minutes(1));
});

it('should accept payments within cap', async function () {
await this.crowdsale.send(this.cap.minus(this.lessThanCap)).should.be.fulfilled;
await this.crowdsale.send(this.lessThanCap).should.be.fulfilled;
});

it('should reject payments outside cap', async function () {
await this.crowdsale.send(this.cap);
await this.crowdsale.send(1).should.be.rejectedWith(EVMRevert);
});

it('should reject payments that exceed cap', async function () {
await this.crowdsale.send(this.cap.plus(1)).should.be.rejectedWith(EVMRevert);
});
});

describe('ending', function () {
beforeEach(async function () {
await increaseTimeTo(this.startTime);
});

it('should not be ended if under cap', async function () {
let hasEnded = await this.crowdsale.hasEnded();
hasEnded.should.equal(false);
await this.crowdsale.send(this.lessThanCap);
hasEnded = await this.crowdsale.hasEnded();
hasEnded.should.equal(false);
});

it('should not be ended if just under cap', async function () {
await this.crowdsale.send(this.cap.minus(1));
let hasEnded = await this.crowdsale.hasEnded();
hasEnded.should.equal(false);
});

it('should be ended if cap reached', async function () {
await this.crowdsale.send(this.cap);
let hasEnded = await this.crowdsale.hasEnded();
hasEnded.should.equal(true);
});
});
});
55 changes: 55 additions & 0 deletions test/token/ERC20/PseudoMinter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import EVMRevert from '../../helpers/EVMRevert';

const BigNumber = web3.BigNumber;

require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();

var PseudoMinter = artifacts.require('./token/ERC20/PseudoMinter.sol');
var SimpleToken = artifacts.require('./example/SimpleToken.sol');

contract('PseudoMinter', function ([owner, tokenAddress]) {
beforeEach(async function () {
this.simpleToken = await SimpleToken.new();
this.pseudoMinter = await PseudoMinter.new(this.simpleToken.address, owner);

this.cap = await this.simpleToken.totalSupply();
this.lessThanCap = 1;

await this.simpleToken.approve(this.pseudoMinter.address, this.cap);
});

it('should accept minting within approved cap', async function () {
await this.pseudoMinter.mint(tokenAddress, this.cap.minus(this.lessThanCap)).should.be.fulfilled;
await this.pseudoMinter.mint(tokenAddress, this.lessThanCap).should.be.fulfilled;

let amount = await this.simpleToken.balanceOf(tokenAddress);
amount.should.be.bignumber.equal(this.cap);
});

it('should reject minting outside approved cap', async function () {
await this.pseudoMinter.mint(tokenAddress, this.cap).should.be.fulfilled;
await this.pseudoMinter.mint(tokenAddress, 1).should.be.rejectedWith(EVMRevert);

let amount = await this.simpleToken.balanceOf(tokenAddress);
amount.should.be.bignumber.equal(this.cap);
});

it('should reject minting that exceed approved cap', async function () {
await this.pseudoMinter.mint(tokenAddress, this.cap.plus(1)).should.be.rejectedWith(EVMRevert);

let amount = await this.simpleToken.balanceOf(tokenAddress);
amount.should.be.bignumber.equal(0);
});

it('should be able to change cap by calling tokens approve function', async function () {
await this.simpleToken.approve(this.pseudoMinter.address, 0);

let amount = await this.pseudoMinter.availableSupply.call();
amount.should.be.bignumber.equal(0);

await this.pseudoMinter.mint(tokenAddress, 1).should.be.rejectedWith(EVMRevert);
});
});