Browse Source

Merge pull request #283 from OpenZeppelin/crowdsale

Add Crowdsale contracts
Manuel Aráoz 8 years ago
parent
commit
e748c3ea36

+ 1 - 2
.travis.yml

@@ -7,7 +7,6 @@ node_js:
 cache:
   yarn: true
 script:
-  - testrpc > /dev/null &
-  - truffle test
+  - yarn test
 after_script:
   - yarn run coveralls

+ 33 - 0
contracts/crowdsale/CappedCrowdsale.sol

@@ -0,0 +1,33 @@
+pragma solidity ^0.4.11;
+
+import '../SafeMath.sol';
+import './Crowdsale.sol';
+
+/**
+ * @title CappedCrowdsale
+ * @dev Extension of Crowsdale with a max amount of funds raised
+ */
+contract CappedCrowdsale is Crowdsale {
+  using SafeMath for uint256;
+
+  uint256 public cap;
+
+  function CappedCrowdsale(uint256 _cap) {
+    cap = _cap;
+  }
+
+  // overriding Crowdsale#validPurchase to add extra cap logic
+  // @return true if investors can buy at the moment
+  function validPurchase() internal constant returns (bool) {
+    bool withinCap = weiRaised.add(msg.value) <= cap;
+    return super.validPurchase() && withinCap;
+  }
+
+  // overriding Crowdsale#hasEnded to add cap logic
+  // @return true if crowdsale event has ended
+  function hasEnded() public constant returns (bool) {
+    bool capReached = weiRaised >= cap;
+    return super.hasEnded() || capReached;
+  }
+
+}

+ 108 - 0
contracts/crowdsale/Crowdsale.sol

@@ -0,0 +1,108 @@
+pragma solidity ^0.4.11;
+
+import '../token/MintableToken.sol';
+import '../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
+ * 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.
+ */
+contract Crowdsale {
+  using SafeMath for uint256;
+
+  // The token being sold
+  MintableToken public token;
+
+  // start and end block where investments are allowed (both inclusive)
+  uint256 public startBlock;
+  uint256 public endBlock;
+
+  // address where funds are collected
+  address public wallet;
+
+  // how many token units a buyer gets per wei
+  uint256 public rate;
+
+  // amount of raised money in wei
+  uint256 public weiRaised;
+
+  /**
+   * event for token purchase logging
+   * @param purchaser who paid for the tokens
+   * @param beneficiary who got the tokens
+   * @param value weis paid for purchase
+   * @param amount amount of tokens purchased
+   */ 
+  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);
+    require(_rate > 0);
+    require(_wallet != 0x0);
+
+    token = createTokenContract();
+    startBlock = _startBlock;
+    endBlock = _endBlock;
+    rate = _rate;
+    wallet = _wallet;
+  }
+
+  // creates the token to be sold. 
+  // override this method to have crowdsale of a specific mintable token.
+  function createTokenContract() internal returns (MintableToken) {
+    return new MintableToken();
+  }
+
+
+  // fallback function can be used to buy tokens
+  function () payable {
+    buyTokens(msg.sender);
+  }
+
+  // low level token purchase function
+  function buyTokens(address beneficiary) payable {
+    require(beneficiary != 0x0);
+    require(validPurchase());
+
+    uint256 weiAmount = msg.value;
+    uint256 updatedWeiRaised = weiRaised.add(weiAmount);
+
+    // calculate token amount to be created
+    uint256 tokens = weiAmount.mul(rate);
+
+    // update state
+    weiRaised = updatedWeiRaised;
+
+    token.mint(beneficiary, tokens);
+    TokenPurchase(msg.sender, beneficiary, weiAmount, tokens);
+
+    forwardFunds();
+  }
+
+  // send ether to the fund collection wallet
+  // override to create custom fund forwarding mechanisms
+  function forwardFunds() internal {
+    wallet.transfer(msg.value);
+  }
+
+  // @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 nonZeroPurchase = msg.value != 0;
+    return withinPeriod && nonZeroPurchase;
+  }
+
+  // @return true if crowdsale event has ended
+  function hasEnded() public constant returns (bool) {
+    return block.number > endBlock;
+  }
+
+
+}

+ 39 - 0
contracts/crowdsale/FinalizableCrowdsale.sol

@@ -0,0 +1,39 @@
+pragma solidity ^0.4.11;
+
+import '../SafeMath.sol';
+import '../ownership/Ownable.sol';
+import './Crowdsale.sol';
+
+/**
+ * @title FinalizableCrowdsale
+ * @dev Extension of Crowsdale where an owner can do extra work
+ * after finishing. By default, it will end token minting.
+ */
+contract FinalizableCrowdsale is Crowdsale, Ownable {
+  using SafeMath for uint256;
+
+  bool public isFinalized = false;
+
+  event Finalized();
+
+  // should be called after crowdsale ends, to do
+  // some extra finalization work
+  function finalize() onlyOwner {
+    require(!isFinalized);
+    require(hasEnded());
+
+    finalization();
+    Finalized();
+    
+    isFinalized = true;
+  }
+
+  // end token minting on finalization
+  // override this with custom logic if needed
+  function finalization() internal {
+    token.finishMinting();
+  }
+
+
+
+}

+ 56 - 0
contracts/crowdsale/RefundVault.sol

@@ -0,0 +1,56 @@
+pragma solidity ^0.4.11;
+
+import '../SafeMath.sol';
+import '../ownership/Ownable.sol';
+
+/**
+ * @title RefundVault
+ * @dev This contract is used for storing funds while a crowdsale
+ * is in progress. Supports refunding the money if crowdsale fails,
+ * and forwarding it if crowdsale is successful.
+ */
+contract RefundVault is Ownable {
+  using SafeMath for uint256;
+
+  enum State { Active, Refunding, Closed }
+
+  mapping (address => uint256) public deposited;
+  address public wallet;
+  State public state;
+
+  event Closed();
+  event RefundsEnabled();
+  event Refunded(address indexed beneficiary, uint256 weiAmount);
+
+  function RefundVault(address _wallet) {
+    require(_wallet != 0x0);
+    wallet = _wallet;
+    state = State.Active;
+  }
+
+  function deposit(address investor) onlyOwner payable {
+    require(state == State.Active);
+    deposited[investor] = deposited[investor].add(msg.value);
+  }
+
+  function close() onlyOwner {
+    require(state == State.Active);
+    state = State.Closed;
+    Closed();
+    wallet.transfer(this.balance);
+  }
+
+  function enableRefunds() onlyOwner {
+    require(state == State.Active);
+    state = State.Refunding;
+    RefundsEnabled();
+  }
+
+  function refund(address investor) {
+    require(state == State.Refunding);
+    uint256 depositedValue = deposited[investor];
+    deposited[investor] = 0;
+    investor.transfer(depositedValue);
+    Refunded(investor, depositedValue);
+  }
+}

+ 59 - 0
contracts/crowdsale/RefundableCrowdsale.sol

@@ -0,0 +1,59 @@
+pragma solidity ^0.4.11;
+
+
+import '../SafeMath.sol';
+import './FinalizableCrowdsale.sol';
+import './RefundVault.sol';
+
+
+/**
+ * @title RefundableCrowdsale
+ * @dev Extension of Crowdsale contract that adds a funding goal, and
+ * the possibility of users getting a refund if goal is not met.
+ * Uses a RefundVault as the crowdsale's vault.
+ */
+contract RefundableCrowdsale is FinalizableCrowdsale {
+  using SafeMath for uint256;
+
+  // minimum amount of funds to be raised in weis
+  uint256 public goal;
+
+  // refund vault used to hold funds while crowdsale is running
+  RefundVault public vault;
+
+  function RefundableCrowdsale(uint256 _goal) {
+    vault = new RefundVault(wallet);
+    goal = _goal;
+  }
+
+  // We're overriding the fund forwarding from Crowdsale.
+  // In addition to sending the funds, we want to call
+  // the RefundVault deposit function
+  function forwardFunds() internal {
+    vault.deposit.value(msg.value)(msg.sender);
+  }
+
+  // if crowdsale is unsuccessful, investors can claim refunds here
+  function claimRefund() {
+    require(isFinalized);
+    require(!goalReached());
+
+    vault.refund(msg.sender);
+  }
+
+  // vault finalization task, called when owner calls finalize()
+  function finalization() internal {
+    if (goalReached()) {
+      vault.close();
+    } else {
+      vault.enableRefunds();
+    }
+
+    super.finalization();
+  }
+
+  function goalReached() public constant returns (bool) {
+    return weiRaised >= goal;
+  }
+
+}

+ 0 - 60
contracts/token/CrowdsaleToken.sol

@@ -1,60 +0,0 @@
-pragma solidity ^0.4.11;
-
-
-import "./StandardToken.sol";
-
-
-/**
- * @title CrowdsaleToken
- *
- * @dev Simple ERC20 Token example, with crowdsale token creation
- * @dev IMPORTANT NOTE: do not use or deploy this contract as-is. It needs some changes to be 
- * production ready.
- */
-contract CrowdsaleToken is StandardToken {
-
-  string public constant name = "CrowdsaleToken";
-  string public constant symbol = "CRW";
-  uint256 public constant decimals = 18;
-  // replace with your fund collection multisig address
-  address public constant multisig = 0x0;
-
-
-  // 1 ether = 500 example tokens
-  uint256 public constant PRICE = 500;
-
-  /**
-   * @dev Fallback function which receives ether and sends the appropriate number of tokens to the 
-   * msg.sender.
-   */
-  function () payable {
-    createTokens(msg.sender);
-  }
-
-  /**
-   * @dev Creates tokens and send to the specified address.
-   * @param recipient The address which will recieve the new tokens.
-   */
-  function createTokens(address recipient) payable {
-    if (msg.value == 0) {
-      throw;
-    }
-
-    uint256 tokens = msg.value.mul(getPrice());
-    totalSupply = totalSupply.add(tokens);
-
-    balances[recipient] = balances[recipient].add(tokens);
-
-    if (!multisig.send(msg.value)) {
-      throw;
-    }
-  }
-
-  /**
-   * @dev replace this with any other price function
-   * @return The price per unit of token. 
-   */
-  function getPrice() constant returns (uint256 result) {
-    return PRICE;
-  }
-}

+ 2 - 6
contracts/token/PausableToken.sol

@@ -7,19 +7,15 @@ import '../lifecycle/Pausable.sol';
  * Pausable token
  *
  * Simple ERC20 Token example, with pausable token creation
- * Issue:
- * https://github.com/OpenZeppelin/zeppelin-solidity/issues/194
- * Based on code by BCAPtoken:
- * https://github.com/BCAPtoken/BCAPToken/blob/5cb5e76338cc47343ba9268663a915337c8b268e/sol/BCAPToken.sol#L27
  **/
 
 contract PausableToken is StandardToken, Pausable {
 
-  function transfer(address _to, uint256 _value) whenNotPaused {
+  function transfer(address _to, uint _value) whenNotPaused {
     super.transfer(_to, _value);
   }
 
-  function transferFrom(address _from, address _to, uint256 _value) whenNotPaused {
+  function transferFrom(address _from, address _to, uint _value) whenNotPaused {
     super.transferFrom(_from, _to, _value);
   }
 }

+ 0 - 14
docs/source/crowdsaletoken.rst

@@ -1,14 +0,0 @@
-CrowdsaleToken
-=============================================
-
-Simple ERC20 Token example, with crowdsale token creation.
-
-Inherits from contract StandardToken.
-
-createTokens(address recipient) payable
-"""""""""""""""""""""""""""""""""""""""""
-Creates tokens based on message value and credits to the recipient.
-
-getPrice() constant returns (uint result)
-"""""""""""""""""""""""""""""""""""""""""
-Returns the amount of tokens per 1 ether.

+ 14 - 2
scripts/test.sh

@@ -4,10 +4,22 @@ output=$(nc -z localhost 8545; echo $?)
 [ $output -eq "0" ] && trpc_running=true
 if [ ! $trpc_running ]; then
   echo "Starting our own testrpc node instance"
-  testrpc > /dev/null &
+  # we give each account 1M ether, needed for high-value tests
+  testrpc \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000"  \
+    --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000"  \
+  > /dev/null &
   trpc_pid=$!
 fi
-./node_modules/truffle/cli.js test
+./node_modules/truffle/cli.js test "$@"
 if [ ! $trpc_running ]; then
   kill -9 $trpc_pid
 fi

+ 81 - 0
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)
+    })
+
+  })
+
+})

+ 143 - 0
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)
+    })
+
+  })
+
+})

+ 61 - 0
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)
+  })
+
+})

+ 1 - 1
test/MultisigWallet.js

@@ -52,7 +52,7 @@ contract('MultisigWallet', function(accounts) {
 
     //Balance of account2 should have increased
     let newAccountBalance = web3.eth.getBalance(accounts[2]);
-    assert.isTrue(newAccountBalance > accountBalance);
+    assert.isTrue(newAccountBalance.greaterThan(accountBalance));
   });
 
   it('should prevent execution of transaction if above daily limit', async function() {

+ 61 - 0
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)
+  })
+
+})

+ 67 - 0
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)
+  })
+
+})

+ 21 - 0
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) 
+  {
+  }
+
+}

+ 1 - 0
test/helpers/EVMThrow.js

@@ -0,0 +1 @@
+export default 'invalid opcode'

+ 20 - 0
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() 
+  {
+  }
+
+}

+ 21 - 0
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) 
+  {
+  }
+
+}

+ 22 - 0
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()
+  }
+}

+ 3 - 0
test/helpers/ether.js

@@ -0,0 +1,3 @@
+export default function ether(n) {
+  return new web3.BigNumber(web3.toWei(n, 'ether'))
+}