Browse Source

add TokenVesting contract

Francisco Giordano 8 years ago
parent
commit
80e591f487
2 changed files with 140 additions and 0 deletions
  1. 82 0
      contracts/token/TokenVesting.sol
  2. 58 0
      test/TokenVesting.js

+ 82 - 0
contracts/token/TokenVesting.sol

@@ -0,0 +1,82 @@
+pragma solidity ^0.4.11;
+
+import './ERC20Basic.sol';
+import '../ownership/Ownable.sol';
+import '../math/Math.sol';
+import '../math/SafeMath.sol';
+
+/**
+ * @title TokenVesting
+ * @dev A token holder contract that can release its token balance gradually like a
+ * typical vesting scheme, with a cliff and vesting period. Revokable by the owner.
+ */
+contract TokenVesting is Ownable {
+  using SafeMath for uint256;
+
+  // beneficiary of tokens after they are released
+  address beneficiary;
+
+  uint256 cliff;
+  uint256 start;
+  uint256 end;
+
+  mapping (address => uint256) released;
+
+  /**
+   * @dev Creates a vesting contract that vests its balance of any ERC20 token to the
+   * _beneficiary, gradually in a linear fashion until _end. By then all of the balance
+   * will have vested.
+   * @param _beneficiary address of the beneficiary to whom vested tokens are transferred
+   * @param _cliff timestamp of the moment when tokens will begin to vest
+   * @param _end timestamp of the moment when all balance will have been vested
+   */
+  function TokenVesting(address _beneficiary, uint256 _cliff, uint256 _end) {
+    beneficiary = _beneficiary;
+    cliff = _cliff;
+    end = _end;
+    start = now;
+  }
+
+  /**
+   * @notice Transfers vested tokens to beneficiary.
+   * @param token ERC20 token which is being vested
+   */
+  function release(ERC20Basic token) {
+    uint256 vested = vestedAmount(token);
+
+    require(vested > 0);
+
+    token.transfer(beneficiary, vested);
+
+    released[token] = released[token].add(vested);
+  }
+
+  /**
+   * @notice Allows the owner to revoke the vesting. Tokens already vested remain in the contract.
+   * @param token ERC20 token which is being vested
+   */
+  function revoke(ERC20Basic token) onlyOwner {
+    uint256 balance = token.balanceOf(this);
+
+    uint256 vested = vestedAmount(token);
+
+    token.transfer(owner, balance - vested);
+  }
+
+  /**
+   * @dev Calculates the amount that has already vested.
+   * @param token ERC20 token which is being vested
+   */
+  function vestedAmount(ERC20Basic token) constant returns (uint256) {
+    if (now < cliff) {
+      return 0;
+    } else {
+      uint256 currentBalance = token.balanceOf(this);
+      uint256 totalBalance = currentBalance.add(released[token]);
+
+      uint256 vested = totalBalance.mul(now - start).div(end - start);
+
+      return Math.min256(currentBalance, vested);
+    }
+  }
+}

+ 58 - 0
test/TokenVesting.js

@@ -0,0 +1,58 @@
+const BigNumber = web3.BigNumber
+
+require('chai')
+  .use(require('chai-as-promised'))
+  .use(require('chai-bignumber')(BigNumber))
+  .should();
+
+import EVMThrow from './helpers/EVMThrow'
+import latestTime from './helpers/latestTime';
+import {increaseTimeTo, duration} from './helpers/increaseTime';
+
+const MintableToken = artifacts.require('MintableToken');
+const TokenVesting = artifacts.require('TokenVesting');
+
+contract('TokenVesting', function ([_, owner, beneficiary]) {
+
+  const amount = new BigNumber(1000);
+
+  beforeEach(async function () {
+    this.token = await MintableToken.new({ from: owner });
+
+    this.cliff = latestTime() + duration.years(1);
+    this.end = latestTime() + duration.years(2);
+
+    this.vesting = await TokenVesting.new(beneficiary, this.cliff, this.end, { from: owner });
+
+    this.start = latestTime(); // gets the timestamp at construction
+
+    await this.token.mint(this.vesting.address, amount, { from: owner });
+  });
+
+  it('cannot be released before cliff', async function () {
+    await this.vesting.release(this.token.address).should.be.rejectedWith(EVMThrow);
+  });
+
+  it('can be released after cliff', async function () {
+    await increaseTimeTo(this.cliff + duration.weeks(1));
+    await this.vesting.release(this.token.address).should.be.fulfilled;
+  });
+
+  it('should release proper amount after cliff', async function () {
+    await increaseTimeTo(this.cliff);
+
+    const { receipt } = await this.vesting.release(this.token.address);
+    const releaseTime = web3.eth.getBlock(receipt.blockNumber).timestamp;
+
+    const balance = await this.token.balanceOf(beneficiary);
+    balance.should.bignumber.equal(amount.mul(releaseTime - this.start).div(this.end - this.start).floor());
+  });
+
+  it('should have released all after end', async function () {
+    await increaseTimeTo(this.end);
+    await this.vesting.release(this.token.address);
+    const balance = await this.token.balanceOf(beneficiary);
+    balance.should.bignumber.equal(amount);
+  });
+
+});