diff --git a/package.json b/package.json index e182124b5..d593fd04b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "babel-preset-stage-2": "^6.18.0", "babel-preset-stage-3": "^6.17.0", "babel-register": "^6.23.0", + "chai": "^4.0.2", + "chai-as-promised": "^7.0.0", + "chai-bignumber": "^2.0.0", "coveralls": "^2.13.1", "ethereumjs-testrpc": "^3.0.2", "mocha-lcov-reporter": "^1.3.0", diff --git a/test/CappedCrowdsale.js b/test/CappedCrowdsale.js new file mode 100644 index 000000000..0a24800b4 --- /dev/null +++ b/test/CappedCrowdsale.js @@ -0,0 +1,81 @@ +import ether from './helpers/ether' +import advanceToBlock from './helpers/advanceToBlock' +import EVMThrow from './helpers/EVMThrow' + +const BigNumber = web3.BigNumber + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should() + +const CappedCrowdsale = artifacts.require('./helpers/CappedCrowdsaleImpl.sol') +const MintableToken = artifacts.require('MintableToken') + +contract('CappedCrowdsale', function ([_, wallet]) { + + const rate = new BigNumber(1000) + + const cap = ether(300) + const lessThanCap = ether(60) + + beforeEach(async function () { + this.startBlock = web3.eth.blockNumber + 10 + this.endBlock = web3.eth.blockNumber + 20 + + this.crowdsale = await CappedCrowdsale.new(this.startBlock, this.endBlock, rate, wallet, cap) + + this.token = MintableToken.at(await this.crowdsale.token()) + }) + + describe('accepting payments', function () { + + beforeEach(async function () { + await advanceToBlock(this.startBlock - 1) + }) + + it('should accept payments within cap', async function () { + await this.crowdsale.send(cap.minus(lessThanCap)).should.be.fulfilled + await this.crowdsale.send(lessThanCap).should.be.fulfilled + }) + + it('should reject payments outside cap', async function () { + await this.crowdsale.send(cap) + await this.crowdsale.send(1).should.be.rejectedWith(EVMThrow) + }) + + it('should reject payments that exceed cap', async function () { + await this.crowdsale.send(cap.plus(1)).should.be.rejectedWith(EVMThrow) + }) + + }) + + describe('ending', function () { + + beforeEach(async function () { + await advanceToBlock(this.startBlock - 1) + }) + + it('should not be ended if under cap', async function () { + let hasEnded = await this.crowdsale.hasEnded() + hasEnded.should.equal(false) + await this.crowdsale.send(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(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(cap) + let hasEnded = await this.crowdsale.hasEnded() + hasEnded.should.equal(true) + }) + + }) + +}) diff --git a/test/Crowdsale.js b/test/Crowdsale.js new file mode 100644 index 000000000..3e697b05a --- /dev/null +++ b/test/Crowdsale.js @@ -0,0 +1,143 @@ +import ether from './helpers/ether' +import advanceToBlock from './helpers/advanceToBlock' +import EVMThrow from './helpers/EVMThrow' + +const BigNumber = web3.BigNumber + +const should = require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should() + +const Crowdsale = artifacts.require('Crowdsale') +const MintableToken = artifacts.require('MintableToken') + +contract('Crowdsale', function ([_, investor, wallet, purchaser]) { + + const rate = new BigNumber(1000) + const value = ether(42) + + const expectedTokenAmount = rate.mul(value) + + beforeEach(async function () { + this.startBlock = web3.eth.blockNumber + 10 + this.endBlock = web3.eth.blockNumber + 20 + + this.crowdsale = await Crowdsale.new(this.startBlock, this.endBlock, rate, wallet) + + this.token = MintableToken.at(await this.crowdsale.token()) + }) + + it('should be token owner', async function () { + const owner = await this.token.owner() + owner.should.equal(this.crowdsale.address) + }) + + it('should be ended only after end', async function () { + let ended = await this.crowdsale.hasEnded() + ended.should.equal(false) + await advanceToBlock(this.endBlock + 1) + ended = await this.crowdsale.hasEnded() + ended.should.equal(true) + }) + + describe('accepting payments', function () { + + it('should reject payments before start', async function () { + await this.crowdsale.send(value).should.be.rejectedWith(EVMThrow) + await this.crowdsale.buyTokens(investor, value, {from: purchaser}).should.be.rejectedWith(EVMThrow) + }) + + it('should accept payments after start', async function () { + await advanceToBlock(this.startBlock - 1) + await this.crowdsale.send(value).should.be.fulfilled + await this.crowdsale.buyTokens(investor, {value: value, from: purchaser}).should.be.fulfilled + }) + + it('should reject payments after end', async function () { + await advanceToBlock(this.endBlock) + await this.crowdsale.send(value).should.be.rejectedWith(EVMThrow) + await this.crowdsale.buyTokens(investor, {value: value, from: purchaser}).should.be.rejectedWith(EVMThrow) + }) + + }) + + describe('high-level purchase', function () { + + beforeEach(async function() { + await advanceToBlock(this.startBlock) + }) + + it('should log purchase', async function () { + const {logs} = await this.crowdsale.sendTransaction({value: value, from: investor}) + + const event = logs.find(e => e.event === 'TokenPurchase') + + should.exist(event) + event.args.purchaser.should.equal(investor) + event.args.beneficiary.should.equal(investor) + event.args.value.should.be.bignumber.equal(value) + event.args.amount.should.be.bignumber.equal(expectedTokenAmount) + }) + + it('should increase totalSupply', async function () { + await this.crowdsale.send(value) + const totalSupply = await this.token.totalSupply() + totalSupply.should.be.bignumber.equal(expectedTokenAmount) + }) + + it('should assign tokens to sender', async function () { + await this.crowdsale.sendTransaction({value: value, from: investor}) + let balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(expectedTokenAmount) + }) + + it('should forward funds to wallet', async function () { + const pre = web3.eth.getBalance(wallet) + await this.crowdsale.sendTransaction({value, from: investor}) + const post = web3.eth.getBalance(wallet) + post.minus(pre).should.be.bignumber.equal(value) + }) + + }) + + describe('low-level purchase', function () { + + beforeEach(async function() { + await advanceToBlock(this.startBlock) + }) + + it('should log purchase', async function () { + const {logs} = await this.crowdsale.buyTokens(investor, {value: value, from: purchaser}) + + const event = logs.find(e => e.event === 'TokenPurchase') + + should.exist(event) + event.args.purchaser.should.equal(purchaser) + event.args.beneficiary.should.equal(investor) + event.args.value.should.be.bignumber.equal(value) + event.args.amount.should.be.bignumber.equal(expectedTokenAmount) + }) + + it('should increase totalSupply', async function () { + await this.crowdsale.buyTokens(investor, {value, from: purchaser}) + const totalSupply = await this.token.totalSupply() + totalSupply.should.be.bignumber.equal(expectedTokenAmount) + }) + + it('should assign tokens to beneficiary', async function () { + await this.crowdsale.buyTokens(investor, {value, from: purchaser}) + const balance = await this.token.balanceOf(investor) + balance.should.be.bignumber.equal(expectedTokenAmount) + }) + + it('should forward funds to wallet', async function () { + const pre = web3.eth.getBalance(wallet) + await this.crowdsale.buyTokens(investor, {value, from: purchaser}) + const post = web3.eth.getBalance(wallet) + post.minus(pre).should.be.bignumber.equal(value) + }) + + }) + +}) diff --git a/test/FinalizableCrowdsale.js b/test/FinalizableCrowdsale.js new file mode 100644 index 000000000..53224bb76 --- /dev/null +++ b/test/FinalizableCrowdsale.js @@ -0,0 +1,61 @@ +import advanceToBlock from './helpers/advanceToBlock' +import EVMThrow from './helpers/EVMThrow' + +const BigNumber = web3.BigNumber + +const should = require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should() + +const FinalizableCrowdsale = artifacts.require('./helpers/FinalizableCrowdsaleImpl.sol') +const MintableToken = artifacts.require('MintableToken') + +contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) { + + const rate = new BigNumber(1000) + + beforeEach(async function () { + this.startBlock = web3.eth.blockNumber + 10 + this.endBlock = web3.eth.blockNumber + 20 + + this.crowdsale = await FinalizableCrowdsale.new(this.startBlock, this.endBlock, rate, wallet, {from: owner}) + + this.token = MintableToken.at(await this.crowdsale.token()) + }) + + it('cannot be finalized before ending', async function () { + await this.crowdsale.finalize({from: owner}).should.be.rejectedWith(EVMThrow) + }) + + it('cannot be finalized by third party after ending', async function () { + await advanceToBlock(this.endBlock) + await this.crowdsale.finalize({from: thirdparty}).should.be.rejectedWith(EVMThrow) + }) + + it('can be finalized by owner after ending', async function () { + await advanceToBlock(this.endBlock) + await this.crowdsale.finalize({from: owner}).should.be.fulfilled + }) + + it('cannot be finalized twice', async function () { + await advanceToBlock(this.endBlock + 1) + await this.crowdsale.finalize({from: owner}) + await this.crowdsale.finalize({from: owner}).should.be.rejectedWith(EVMThrow) + }) + + it('logs finalized', async function () { + await advanceToBlock(this.endBlock) + const {logs} = await this.crowdsale.finalize({from: owner}) + const event = logs.find(e => e.event === 'Finalized') + should.exist(event) + }) + + it('finishes minting of token', async function () { + await advanceToBlock(this.endBlock) + await this.crowdsale.finalize({from: owner}) + const finished = await this.token.mintingFinished() + finished.should.equal(true) + }) + +}) diff --git a/test/RefundVault.js b/test/RefundVault.js new file mode 100644 index 000000000..2102df980 --- /dev/null +++ b/test/RefundVault.js @@ -0,0 +1,61 @@ +const BigNumber = web3.BigNumber + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should() + +import ether from './helpers/ether' +import EVMThrow from './helpers/EVMThrow' + +const RefundVault = artifacts.require('RefundVault') + +contract('RefundVault', function ([_, owner, wallet, investor]) { + + const value = ether(42) + + beforeEach(async function () { + this.vault = await RefundVault.new(wallet, {from: owner}) + }) + + it('should accept contributions', async function () { + await this.vault.deposit(investor, {value, from: owner}).should.be.fulfilled + }) + + it('should not refund contribution during active state', async function () { + await this.vault.deposit(investor, {value, from: owner}) + await this.vault.refund(investor).should.be.rejectedWith(EVMThrow) + }) + + it('only owner can enter refund mode', async function () { + await this.vault.enableRefunds({from: _}).should.be.rejectedWith(EVMThrow) + await this.vault.enableRefunds({from: owner}).should.be.fulfilled + }) + + it('should refund contribution after entering refund mode', async function () { + await this.vault.deposit(investor, {value, from: owner}) + await this.vault.enableRefunds({from: owner}) + + const pre = web3.eth.getBalance(investor) + await this.vault.refund(investor) + const post = web3.eth.getBalance(investor) + + post.minus(pre).should.be.bignumber.equal(value) + }) + + it('only owner can close', async function () { + await this.vault.close({from: _}).should.be.rejectedWith(EVMThrow) + await this.vault.close({from: owner}).should.be.fulfilled + }) + + it('should forward funds to wallet after closing', async function () { + await this.vault.deposit(investor, {value, from: owner}) + + const pre = web3.eth.getBalance(wallet) + await this.vault.close({from: owner}) + const post = web3.eth.getBalance(wallet) + + post.minus(pre).should.be.bignumber.equal(value) + }) + +}) diff --git a/test/RefundableCrowdsale.js b/test/RefundableCrowdsale.js new file mode 100644 index 000000000..76f2e22c3 --- /dev/null +++ b/test/RefundableCrowdsale.js @@ -0,0 +1,67 @@ +import ether from './helpers/ether' +import advanceToBlock from './helpers/advanceToBlock' +import EVMThrow from './helpers/EVMThrow' + +const BigNumber = web3.BigNumber + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should() + +const RefundableCrowdsale = artifacts.require('./helpers/RefundableCrowdsaleImpl.sol') + +contract('RefundableCrowdsale', function ([_, owner, wallet, investor]) { + + const rate = new BigNumber(1000) + const goal = ether(800) + const lessThanGoal = ether(750) + + beforeEach(async function () { + this.startBlock = web3.eth.blockNumber + 10 + this.endBlock = web3.eth.blockNumber + 20 + + this.crowdsale = await RefundableCrowdsale.new(this.startBlock, this.endBlock, rate, wallet, goal, {from: owner}) + }) + + it('should deny refunds before end', async function () { + await this.crowdsale.claimRefund({from: investor}).should.be.rejectedWith(EVMThrow) + await advanceToBlock(this.endBlock - 1) + await this.crowdsale.claimRefund({from: investor}).should.be.rejectedWith(EVMThrow) + }) + + it('should deny refunds after end if goal was reached', async function () { + await advanceToBlock(this.startBlock - 1) + await this.crowdsale.sendTransaction({value: goal, from: investor}) + await advanceToBlock(this.endBlock) + await this.crowdsale.claimRefund({from: investor}).should.be.rejectedWith(EVMThrow) + }) + + it('should allow refunds after end if goal was not reached', async function () { + await advanceToBlock(this.startBlock - 1) + await this.crowdsale.sendTransaction({value: lessThanGoal, from: investor}) + await advanceToBlock(this.endBlock) + + await this.crowdsale.finalize({from: owner}) + + const pre = web3.eth.getBalance(investor) + await this.crowdsale.claimRefund({from: investor, gasPrice: 0}) + .should.be.fulfilled + const post = web3.eth.getBalance(investor) + + post.minus(pre).should.be.bignumber.equal(lessThanGoal) + }) + + it('should forward funds to wallet after end if goal was reached', async function () { + await advanceToBlock(this.startBlock - 1) + await this.crowdsale.sendTransaction({value: goal, from: investor}) + await advanceToBlock(this.endBlock) + + const pre = web3.eth.getBalance(wallet) + await this.crowdsale.finalize({from: owner}) + const post = web3.eth.getBalance(wallet) + + post.minus(pre).should.be.bignumber.equal(goal) + }) + +}) diff --git a/test/helpers/CappedCrowdsaleImpl.sol b/test/helpers/CappedCrowdsaleImpl.sol new file mode 100644 index 000000000..6a255dca3 --- /dev/null +++ b/test/helpers/CappedCrowdsaleImpl.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.4.11; + + +import '../../contracts/crowdsale/CappedCrowdsale.sol'; + + +contract CappedCrowdsaleImpl is CappedCrowdsale { + + function CappedCrowdsaleImpl ( + uint256 _startBlock, + uint256 _endBlock, + uint256 _rate, + address _wallet, + uint256 _cap + ) + Crowdsale(_startBlock, _endBlock, _rate, _wallet) + CappedCrowdsale(_cap) + { + } + +} diff --git a/test/helpers/EVMThrow.js b/test/helpers/EVMThrow.js new file mode 100644 index 000000000..c75924f52 --- /dev/null +++ b/test/helpers/EVMThrow.js @@ -0,0 +1 @@ +export default 'invalid opcode' diff --git a/test/helpers/FinalizableCrowdsaleImpl.sol b/test/helpers/FinalizableCrowdsaleImpl.sol new file mode 100644 index 000000000..b35b633a9 --- /dev/null +++ b/test/helpers/FinalizableCrowdsaleImpl.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.4.11; + + +import '../../contracts/crowdsale/FinalizableCrowdsale.sol'; + + +contract FinalizableCrowdsaleImpl is FinalizableCrowdsale { + + function FinalizableCrowdsaleImpl ( + uint256 _startBlock, + uint256 _endBlock, + uint256 _rate, + address _wallet + ) + Crowdsale(_startBlock, _endBlock, _rate, _wallet) + FinalizableCrowdsale() + { + } + +} diff --git a/test/helpers/RefundableCrowdsaleImpl.sol b/test/helpers/RefundableCrowdsaleImpl.sol new file mode 100644 index 000000000..5250f56f8 --- /dev/null +++ b/test/helpers/RefundableCrowdsaleImpl.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.4.11; + + +import '../../contracts/crowdsale/RefundableCrowdsale.sol'; + + +contract RefundableCrowdsaleImpl is RefundableCrowdsale { + + function RefundableCrowdsaleImpl ( + uint256 _startBlock, + uint256 _endBlock, + uint256 _rate, + address _wallet, + uint256 _goal + ) + Crowdsale(_startBlock, _endBlock, _rate, _wallet) + RefundableCrowdsale(_goal) + { + } + +} diff --git a/test/helpers/advanceToBlock.js b/test/helpers/advanceToBlock.js new file mode 100644 index 000000000..97d723f19 --- /dev/null +++ b/test/helpers/advanceToBlock.js @@ -0,0 +1,22 @@ +export function advanceBlock() { + return new Promise((resolve, reject) => { + web3.currentProvider.sendAsync({ + jsonrpc: '2.0', + method: 'evm_mine', + id: Date.now(), + }, (err, res) => { + return err ? reject(err) : resolve(res) + }) + }) +} + +// Advances the block number so that the last mined block is `number`. +export default async function advanceToBlock(number) { + if (web3.eth.blockNumber > number) { + throw Error(`block number ${number} is in the past (current is ${web3.eth.blockNumber})`) + } + + while (web3.eth.blockNumber < number) { + await advanceBlock() + } +} diff --git a/test/helpers/ether.js b/test/helpers/ether.js new file mode 100644 index 000000000..e35f90320 --- /dev/null +++ b/test/helpers/ether.js @@ -0,0 +1,3 @@ +export default function ether(n) { + return new web3.BigNumber(web3.toWei(n, 'ether')) +}