Skip to content
Closed
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);
});
});