Browse Source

Merge pull request #353 from jakub-wojciechowski/master

Change crowdsales to use timestamps instead of block numbers #350
Francisco Giordano 8 years ago
parent
commit
2b079136fb

+ 11 - 12
contracts/crowdsale/Crowdsale.sol

@@ -6,7 +6,7 @@ import '../math/SafeMath.sol';
 /**
  * @title Crowdsale 
  * @dev Crowdsale is a base contract for managing a token crowdsale.
- * Crowdsales have a start and end block, where investors can make
+ * 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.
@@ -17,9 +17,9 @@ contract Crowdsale {
   // The token being sold
   MintableToken public token;
 
-  // start and end block where investments are allowed (both inclusive)
-  uint256 public startBlock;
-  uint256 public endBlock;
+  // start and end timestamps where investments are allowed (both inclusive)
+  uint256 public startTime;
+  uint256 public endTime;
 
   // address where funds are collected
   address public wallet;
@@ -40,15 +40,15 @@ contract Crowdsale {
   event TokenPurchase(address indexed purchaser, address indexed beneficiary, uint256 value, uint256 amount);
 
 
-  function Crowdsale(uint256 _startBlock, uint256 _endBlock, uint256 _rate, address _wallet) {
-    require(_startBlock >= block.number);
-    require(_endBlock >= _startBlock);
+  function Crowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, address _wallet) {
+    require(_startTime >= now);
+    require(_endTime >= _startTime);
     require(_rate > 0);
     require(_wallet != 0x0);
 
     token = createTokenContract();
-    startBlock = _startBlock;
-    endBlock = _endBlock;
+    startTime = _startTime;
+    endTime = _endTime;
     rate = _rate;
     wallet = _wallet;
   }
@@ -92,15 +92,14 @@ contract Crowdsale {
 
   // @return true if the transaction can buy tokens
   function validPurchase() internal constant returns (bool) {
-    uint256 current = block.number;
-    bool withinPeriod = current >= startBlock && current <= endBlock;
+    bool withinPeriod = now >= startTime && now <= endTime;
     bool nonZeroPurchase = msg.value != 0;
     return withinPeriod && nonZeroPurchase;
   }
 
   // @return true if crowdsale event has ended
   function hasEnded() public constant returns (bool) {
-    return block.number > endBlock;
+    return now > endTime;
   }
 
 

+ 2 - 2
contracts/examples/SampleCrowdsale.sol

@@ -30,11 +30,11 @@ contract SampleCrowdsaleToken is MintableToken {
  */
 contract SampleCrowdsale is CappedCrowdsale, RefundableCrowdsale {
 
-  function SampleCrowdsale(uint256 _startBlock, uint256 _endBlock, uint256 _rate, uint256 _goal, uint256 _cap, address _wallet)
+  function SampleCrowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, uint256 _goal, uint256 _cap, address _wallet)
     CappedCrowdsale(_cap)
     FinalizableCrowdsale()
     RefundableCrowdsale(_goal)
-    Crowdsale(_startBlock, _endBlock, _rate, _wallet)
+    Crowdsale(_startTime, _endTime, _rate, _wallet)
   {
     //As goal needs to be met for a successful crowdsale
     //the value needs to less or equal than a cap which is limit for accepted funds

+ 20 - 14
test/CappedCrowdsale.js

@@ -1,5 +1,7 @@
 import ether from './helpers/ether'
-import advanceToBlock from './helpers/advanceToBlock'
+import {advanceBlock} from './helpers/advanceToBlock'
+import {increaseTimeTo, duration} from './helpers/increaseTime'
+import latestTime from './helpers/latestTime'
 import EVMThrow from './helpers/EVMThrow'
 
 const BigNumber = web3.BigNumber
@@ -19,28 +21,32 @@ contract('CappedCrowdsale', function ([_, wallet]) {
   const cap = ether(300)
   const lessThanCap = ether(60)
 
-  describe('creating a valid crowdsale', function () {
-
-    it('should fail with zero cap', async function () {
-      await CappedCrowdsale.new(this.startBlock, this.endBlock, rate, wallet, 0).should.be.rejectedWith(EVMThrow);
-    })
-
-  });
-
+  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.startBlock = web3.eth.blockNumber + 10
-    this.endBlock =   web3.eth.blockNumber + 20
+    this.startTime = latestTime().unix() + duration.weeks(1);
+    this.endTime =   this.startTime + duration.weeks(1);
 
-    this.crowdsale = await CappedCrowdsale.new(this.startBlock, this.endBlock, rate, wallet, cap)
+    this.crowdsale = await CappedCrowdsale.new(this.startTime, this.endTime, rate, wallet, cap)
 
     this.token = MintableToken.at(await this.crowdsale.token())
   })
 
+  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(EVMThrow);
+    })
+
+  });
+
   describe('accepting payments', function () {
 
     beforeEach(async function () {
-      await advanceToBlock(this.startBlock - 1)
+      await increaseTimeTo(this.startTime)
     })
 
     it('should accept payments within cap', async function () {
@@ -62,7 +68,7 @@ contract('CappedCrowdsale', function ([_, wallet]) {
   describe('ending', function () {
 
     beforeEach(async function () {
-      await advanceToBlock(this.startBlock - 1)
+      await increaseTimeTo(this.startTime)
     })
 
     it('should not be ended if under cap', async function () {

+ 24 - 15
test/Crowdsale.js

@@ -1,5 +1,7 @@
 import ether from './helpers/ether'
-import advanceToBlock from './helpers/advanceToBlock'
+import {advanceBlock} from './helpers/advanceToBlock'
+import {increaseTimeTo, duration} from './helpers/increaseTime'
+import latestTime from './helpers/latestTime'
 import EVMThrow from './helpers/EVMThrow'
 
 const BigNumber = web3.BigNumber
@@ -19,11 +21,18 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
 
   const expectedTokenAmount = rate.mul(value)
 
+  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.startBlock = web3.eth.blockNumber + 10
-    this.endBlock =   web3.eth.blockNumber + 20
+    this.startTime = latestTime().unix() + duration.weeks(1);
+    this.endTime =   this.startTime + duration.weeks(1);
+    this.afterEndTime = this.endTime + duration.seconds(1)
+
 
-    this.crowdsale = await Crowdsale.new(this.startBlock, this.endBlock, rate, wallet)
+    this.crowdsale = await Crowdsale.new(this.startTime, this.endTime, rate, wallet)
 
     this.token = MintableToken.at(await this.crowdsale.token())
   })
@@ -36,7 +45,7 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
   it('should be ended only after end', async function () {
     let ended = await this.crowdsale.hasEnded()
     ended.should.equal(false)
-    await advanceToBlock(this.endBlock + 1)
+    await increaseTimeTo(this.afterEndTime)
     ended = await this.crowdsale.hasEnded()
     ended.should.equal(true)
   })
@@ -49,13 +58,13 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
     })
 
     it('should accept payments after start', async function () {
-      await advanceToBlock(this.startBlock - 1)
+      await increaseTimeTo(this.startTime)
       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 increaseTimeTo(this.afterEndTime)
       await this.crowdsale.send(value).should.be.rejectedWith(EVMThrow)
       await this.crowdsale.buyTokens(investor, {value: value, from: purchaser}).should.be.rejectedWith(EVMThrow)
     })
@@ -65,7 +74,7 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
   describe('high-level purchase', function () {
 
     beforeEach(async function() {
-      await advanceToBlock(this.startBlock)
+      await increaseTimeTo(this.startTime)
     })
 
     it('should log purchase', async function () {
@@ -104,33 +113,33 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
   describe('low-level purchase', function () {
 
     beforeEach(async function() {
-      await advanceToBlock(this.startBlock)
+      await increaseTimeTo(this.startTime)
     })
-
+    
     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})

+ 18 - 9
test/FinalizableCrowdsale.js

@@ -1,4 +1,6 @@
-import advanceToBlock from './helpers/advanceToBlock'
+import {advanceBlock} from './helpers/advanceToBlock'
+import {increaseTimeTo, duration} from './helpers/increaseTime'
+import latestTime from './helpers/latestTime'
 import EVMThrow from './helpers/EVMThrow'
 
 const BigNumber = web3.BigNumber
@@ -15,11 +17,18 @@ contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) {
 
   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.startBlock = web3.eth.blockNumber + 10
-    this.endBlock = web3.eth.blockNumber + 20
+    this.startTime = latestTime().unix() + duration.weeks(1)
+    this.endTime =   this.startTime + duration.weeks(1)
+    this.afterEndTime = this.endTime + duration.seconds(1)
+
 
-    this.crowdsale = await FinalizableCrowdsale.new(this.startBlock, this.endBlock, rate, wallet, {from: owner})
+    this.crowdsale = await FinalizableCrowdsale.new(this.startTime, this.endTime, rate, wallet, {from: owner})
 
     this.token = MintableToken.at(await this.crowdsale.token())
   })
@@ -29,30 +38,30 @@ contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) {
   })
 
   it('cannot be finalized by third party after ending', async function () {
-    await advanceToBlock(this.endBlock)
+    await increaseTimeTo(this.afterEndTime)
     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 increaseTimeTo(this.afterEndTime)
     await this.crowdsale.finalize({from: owner}).should.be.fulfilled
   })
 
   it('cannot be finalized twice', async function () {
-    await advanceToBlock(this.endBlock + 1)
+    await increaseTimeTo(this.afterEndTime)
     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)
+    await increaseTimeTo(this.afterEndTime)
     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 increaseTimeTo(this.afterEndTime)
     await this.crowdsale.finalize({from: owner})
     const finished = await this.token.mintingFinished()
     finished.should.equal(true)

+ 24 - 17
test/RefundableCrowdsale.js

@@ -1,5 +1,7 @@
 import ether from './helpers/ether'
-import advanceToBlock from './helpers/advanceToBlock'
+import {advanceBlock} from './helpers/advanceToBlock'
+import {increaseTimeTo, duration} from './helpers/increaseTime'
+import latestTime from './helpers/latestTime'
 import EVMThrow from './helpers/EVMThrow'
 
 const BigNumber = web3.BigNumber
@@ -17,39 +19,44 @@ contract('RefundableCrowdsale', function ([_, owner, wallet, investor]) {
   const goal = ether(800)
   const lessThanGoal = ether(750)
 
+  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().unix() + duration.weeks(1)
+    this.endTime =   this.startTime + duration.weeks(1)
+    this.afterEndTime = this.endTime + duration.seconds(1)
+
+    this.crowdsale = await RefundableCrowdsale.new(this.startTime, this.endTime, rate, wallet, goal, {from: owner})
+  })
+
   describe('creating a valid crowdsale', function () {
 
     it('should fail with zero goal', async function () {
-      await RefundableCrowdsale.new(this.startBlock, this.endBlock, rate, wallet, 0, {from: owner}).should.be.rejectedWith(EVMThrow);
+      await RefundableCrowdsale.new(this.startTime, this.endTime, rate, wallet, 0, {from: owner}).should.be.rejectedWith(EVMThrow);
     })
 
   });
 
-
-  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 increaseTimeTo(this.startTime)
     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 increaseTimeTo(this.startTime)
     await this.crowdsale.sendTransaction({value: goal, from: investor})
-    await advanceToBlock(this.endBlock)
+    await increaseTimeTo(this.afterEndTime)
     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 increaseTimeTo(this.startTime)
     await this.crowdsale.sendTransaction({value: lessThanGoal, from: investor})
-    await advanceToBlock(this.endBlock)
+    await increaseTimeTo(this.afterEndTime)
 
     await this.crowdsale.finalize({from: owner})
 
@@ -62,9 +69,9 @@ contract('RefundableCrowdsale', function ([_, owner, wallet, investor]) {
   })
 
   it('should forward funds to wallet after end if goal was reached', async function () {
-    await advanceToBlock(this.startBlock - 1)
+    await increaseTimeTo(this.startTime)
     await this.crowdsale.sendTransaction({value: goal, from: investor})
-    await advanceToBlock(this.endBlock)
+    await increaseTimeTo(this.afterEndTime)
 
     const pre = web3.eth.getBalance(wallet)
     await this.crowdsale.finalize({from: owner})

+ 21 - 12
test/SampleCrowdsale.js

@@ -1,5 +1,7 @@
 import ether from './helpers/ether'
-import advanceToBlock from './helpers/advanceToBlock'
+import {advanceBlock} from './helpers/advanceToBlock'
+import {increaseTimeTo, duration} from './helpers/increaseTime'
+import latestTime from './helpers/latestTime'
 import EVMThrow from './helpers/EVMThrow'
 
 const BigNumber = web3.BigNumber;
@@ -18,11 +20,17 @@ contract('Crowdsale', function ([owner, wallet, investor]) {
   const GOAL = ether(10);
   const CAP  = ether(20);
 
+  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.startBlock = web3.eth.blockNumber + 10;
-    this.endBlock =   web3.eth.blockNumber + 20;
+    this.startTime = latestTime().unix() + duration.weeks(1);
+    this.endTime =   this.startTime + duration.weeks(1);
+    this.afterEndTime = this.endTime + duration.seconds(1);
 
-    this.crowdsale = await SampleCrowdsale.new(this.startBlock, this.endBlock, RATE, GOAL, CAP, wallet);
+    this.crowdsale = await SampleCrowdsale.new(this.startTime, this.endTime, RATE, GOAL, CAP, wallet);
     this.token = SampleCrowdsaleToken.at(await this.crowdsale.token());
   });
 
@@ -31,8 +39,8 @@ contract('Crowdsale', function ([owner, wallet, investor]) {
     this.crowdsale.should.exist;
     this.token.should.exist;
 
-    (await this.crowdsale.startBlock()).should.be.bignumber.equal(this.startBlock);
-    (await this.crowdsale.endBlock()).should.be.bignumber.equal(this.endBlock);
+    (await this.crowdsale.startTime()).should.be.bignumber.equal(this.startTime);
+    (await this.crowdsale.endTime()).should.be.bignumber.equal(this.endTime);
     (await this.crowdsale.rate()).should.be.bignumber.equal(RATE);
     (await this.crowdsale.wallet()).should.be.equal(wallet);
     (await this.crowdsale.goal()).should.be.bignumber.equal(GOAL);
@@ -48,7 +56,7 @@ contract('Crowdsale', function ([owner, wallet, investor]) {
     const investmentAmount = ether(1);
     const expectedTokenAmount = RATE.mul(investmentAmount);
 
-    await advanceToBlock(this.startBlock - 1);
+    await increaseTimeTo(this.startTime);
     await this.crowdsale.buyTokens(investor, {value: investmentAmount, from: investor}).should.be.fulfilled;
 
     (await this.token.balanceOf(investor)).should.be.bignumber.equal(expectedTokenAmount);
@@ -56,22 +64,23 @@ contract('Crowdsale', function ([owner, wallet, investor]) {
   });
 
   it('should reject payments after end', async function () {
-    await advanceToBlock(this.endBlock);
+    await increaseTimeTo(this.afterEnd);
     await this.crowdsale.send(ether(1)).should.be.rejectedWith(EVMThrow);
     await this.crowdsale.buyTokens(investor, {value: ether(1), from: investor}).should.be.rejectedWith(EVMThrow);
   });
 
   it('should reject payments over cap', async function () {
-    await advanceToBlock(this.startBlock - 1);
+    await increaseTimeTo(this.startTime);
     await this.crowdsale.send(CAP);
     await this.crowdsale.send(1).should.be.rejectedWith(EVMThrow);
   });
 
   it('should allow finalization and transfer funds to wallet if the goal is reached', async function () {
-    await advanceToBlock(this.endBlock - 1);
+    await increaseTimeTo(this.startTime);
     await this.crowdsale.send(GOAL);
 
     const beforeFinalization = web3.eth.getBalance(wallet);
+    await increaseTimeTo(this.afterEndTime);
     await this.crowdsale.finalize({from: owner});
     const afterFinalization = web3.eth.getBalance(wallet);
 
@@ -81,9 +90,9 @@ contract('Crowdsale', function ([owner, wallet, investor]) {
   it('should allow refunds if the goal is not reached', async function () {
     const balanceBeforeInvestment = web3.eth.getBalance(investor);
 
-    await advanceToBlock(this.startBlock - 1);
+    await increaseTimeTo(this.startTime);
     await this.crowdsale.sendTransaction({value: ether(1), from: investor, gasPrice: 0});
-    await advanceToBlock(this.endBlock);
+    await increaseTimeTo(this.afterEndTime);
 
     await this.crowdsale.finalize({from: owner});
     await this.crowdsale.claimRefund({from: investor, gasPrice: 0}).should.be.fulfilled;

+ 4 - 4
test/TokenTimelock.js

@@ -29,26 +29,26 @@ contract('TokenTimelock', function ([_, owner, beneficiary]) {
   })
 
   it('cannot be released just before time limit', async function () {
-    await increaseTime(moment.duration(0.99, 'year'))
+    await increaseTime(moment.duration(0.99, 'year').asSeconds())
     await this.timelock.release().should.be.rejected
   })
 
   it('can be released just after limit', async function () {
-    await increaseTime(moment.duration(1.01, 'year'))
+    await increaseTime(moment.duration(1.01, 'year').asSeconds())
     await this.timelock.release().should.be.fulfilled
     const balance = await this.token.balanceOf(beneficiary)
     balance.should.be.bignumber.equal(amount)
   })
 
   it('can be released after time limit', async function () {
-    await increaseTime(moment.duration(2, 'year'))
+    await increaseTime(moment.duration(2, 'year').asSeconds())
     await this.timelock.release().should.be.fulfilled
     const balance = await this.token.balanceOf(beneficiary)
     balance.should.be.bignumber.equal(amount)
   })
 
   it('cannot be released twice', async function () {
-    await increaseTime(moment.duration(2, 'year'))
+    await increaseTime(moment.duration(2, 'year').asSeconds())
     await this.timelock.release().should.be.fulfilled
     await this.timelock.release().should.be.rejected
     const balance = await this.token.balanceOf(beneficiary)

+ 3 - 3
test/helpers/CappedCrowdsaleImpl.sol

@@ -7,13 +7,13 @@ import '../../contracts/crowdsale/CappedCrowdsale.sol';
 contract CappedCrowdsaleImpl is CappedCrowdsale {
 
   function CappedCrowdsaleImpl (
-    uint256 _startBlock,
-    uint256 _endBlock,
+    uint256 _startTime,
+    uint256 _endTime,
     uint256 _rate,
     address _wallet,
     uint256 _cap
   )
-    Crowdsale(_startBlock, _endBlock, _rate, _wallet)
+    Crowdsale(_startTime, _endTime, _rate, _wallet)
     CappedCrowdsale(_cap) 
   {
   }

+ 3 - 3
test/helpers/FinalizableCrowdsaleImpl.sol

@@ -7,12 +7,12 @@ import '../../contracts/crowdsale/FinalizableCrowdsale.sol';
 contract FinalizableCrowdsaleImpl is FinalizableCrowdsale {
 
   function FinalizableCrowdsaleImpl (
-    uint256 _startBlock,
-    uint256 _endBlock,
+    uint256 _startTime,
+    uint256 _endTime,
     uint256 _rate,
     address _wallet
   )
-    Crowdsale(_startBlock, _endBlock, _rate, _wallet)
+    Crowdsale(_startTime, _endTime, _rate, _wallet)
     FinalizableCrowdsale() 
   {
   }

+ 3 - 3
test/helpers/RefundableCrowdsaleImpl.sol

@@ -7,13 +7,13 @@ import '../../contracts/crowdsale/RefundableCrowdsale.sol';
 contract RefundableCrowdsaleImpl is RefundableCrowdsale {
 
   function RefundableCrowdsaleImpl (
-    uint256 _startBlock,
-    uint256 _endBlock,
+    uint256 _startTime,
+    uint256 _endTime,
     uint256 _rate,
     address _wallet,
     uint256 _goal
   )
-    Crowdsale(_startBlock, _endBlock, _rate, _wallet)
+    Crowdsale(_startTime, _endTime, _rate, _wallet)
     RefundableCrowdsale(_goal) 
   {
   }

+ 26 - 2
test/helpers/increaseTime.js

@@ -1,4 +1,6 @@
-// Increases testrpc time by the passed duration (a moment.js instance)
+import latestTime from './latestTime'
+
+// Increases testrpc time by the passed duration in seconds
 export default function increaseTime(duration) {
   const id = Date.now()
 
@@ -6,7 +8,7 @@ export default function increaseTime(duration) {
     web3.currentProvider.sendAsync({
       jsonrpc: '2.0',
       method: 'evm_increaseTime',
-      params: [duration.asSeconds()],
+      params: [duration],
       id: id,
     }, err1 => {
       if (err1) return reject(err1)
@@ -21,3 +23,25 @@ export default function increaseTime(duration) {
     })
   })
 }
+
+/**
+ * Beware that due to the need of calling two separate testrpc methods and rpc calls overhead
+ * it's hard to increase time precisely to a target point so design your test to tolerate
+ * small fluctuations from time to time.
+ *
+ * @param target time in seconds
+ */
+export function increaseTimeTo(target) {
+  let now = latestTime().unix();
+  if (target < now) throw Error(`Cannot increase current time(${now}) to a moment in the past(${target})`);
+  let diff = target - now;
+  return increaseTime(diff);
+}
+
+export const duration = {
+  seconds: function(val) { return val},
+  minutes: function(val) { return val * this.seconds(60) },
+  hours:   function(val) { return val * this.minutes(60) },
+  days:    function(val) { return val * this.hours(24) },
+  weeks:   function(val) { return val * this.days(7) }
+};