diff --git a/contracts/crowdsale/Crowdsale.sol b/contracts/crowdsale/Crowdsale.sol index 242b67bf960..7e0bf0b7595 100644 --- a/contracts/crowdsale/Crowdsale.sol +++ b/contracts/crowdsale/Crowdsale.sol @@ -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; @@ -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); diff --git a/contracts/examples/PreMintedCrowdsale.sol b/contracts/examples/PreMintedCrowdsale.sol new file mode 100644 index 00000000000..45565ddfdd0 --- /dev/null +++ b/contracts/examples/PreMintedCrowdsale.sol @@ -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; + } +} diff --git a/contracts/examples/SampleCrowdsale.sol b/contracts/examples/SampleCrowdsale.sol index a9d111f8c4b..d5241ef9e66 100644 --- a/contracts/examples/SampleCrowdsale.sol +++ b/contracts/examples/SampleCrowdsale.sol @@ -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) diff --git a/contracts/mocks/CappedCrowdsaleImpl.sol b/contracts/mocks/CappedCrowdsaleImpl.sol index 39a3bc88ed4..31676f99814 100644 --- a/contracts/mocks/CappedCrowdsaleImpl.sol +++ b/contracts/mocks/CappedCrowdsaleImpl.sol @@ -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) diff --git a/contracts/mocks/FinalizableCrowdsaleImpl.sol b/contracts/mocks/FinalizableCrowdsaleImpl.sol index a03a0ad3dfd..748cf416465 100644 --- a/contracts/mocks/FinalizableCrowdsaleImpl.sol +++ b/contracts/mocks/FinalizableCrowdsaleImpl.sol @@ -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) { diff --git a/contracts/mocks/RefundableCrowdsaleImpl.sol b/contracts/mocks/RefundableCrowdsaleImpl.sol index d1744b6de75..0efde0b3da3 100644 --- a/contracts/mocks/RefundableCrowdsaleImpl.sol +++ b/contracts/mocks/RefundableCrowdsaleImpl.sol @@ -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) diff --git a/contracts/token/ERC20/Mintable.sol b/contracts/token/ERC20/Mintable.sol new file mode 100644 index 00000000000..c0ff221b6ea --- /dev/null +++ b/contracts/token/ERC20/Mintable.sol @@ -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); +} diff --git a/contracts/token/ERC20/MintableToken.sol b/contracts/token/ERC20/MintableToken.sol index 21915ea15a7..21a2c862941 100644 --- a/contracts/token/ERC20/MintableToken.sol +++ b/contracts/token/ERC20/MintableToken.sol @@ -1,5 +1,6 @@ pragma solidity ^0.4.18; +import "./Mintable.sol"; import "./StandardToken.sol"; import "../../ownership/Ownable.sol"; @@ -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(); diff --git a/contracts/token/ERC20/PseudoMinter.sol b/contracts/token/ERC20/PseudoMinter.sol new file mode 100644 index 00000000000..d20699a3bba --- /dev/null +++ b/contracts/token/ERC20/PseudoMinter.sol @@ -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); + } +} diff --git a/test/crowdsale/CappedCrowdsale.test.js b/test/crowdsale/CappedCrowdsale.test.js index 34bd423282e..69ef71b1e1d 100644 --- a/test/crowdsale/CappedCrowdsale.test.js +++ b/test/crowdsale/CappedCrowdsale.test.js @@ -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); }); }); diff --git a/test/crowdsale/RefundableCrowdsale.test.js b/test/crowdsale/RefundableCrowdsale.test.js index 63aedd2d691..d2a6530952a 100644 --- a/test/crowdsale/RefundableCrowdsale.test.js +++ b/test/crowdsale/RefundableCrowdsale.test.js @@ -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); }); }); diff --git a/test/examples/PreMintedCrowdsale.test.js b/test/examples/PreMintedCrowdsale.test.js new file mode 100644 index 00000000000..fd515acbc48 --- /dev/null +++ b/test/examples/PreMintedCrowdsale.test.js @@ -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); + }); + }); +}); diff --git a/test/token/ERC20/PseudoMinter.test.js b/test/token/ERC20/PseudoMinter.test.js new file mode 100644 index 00000000000..4c2b0b1b773 --- /dev/null +++ b/test/token/ERC20/PseudoMinter.test.js @@ -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); + }); +});