Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TokenVesting contract #412

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions contracts/token/TokenVesting.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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. Optionally revocable 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;

bool revocable;

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
* @param _revocable whether the vesting is revocable or not
*/
function TokenVesting(address _beneficiary, uint256 _cliff, uint256 _end, bool _revocable) {
require(_beneficiary != 0x0);
require(_cliff > now);
require(_end > _cliff);

beneficiary = _beneficiary;
cliff = _cliff;
end = _end;
revocable = _revocable;

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 {
require(revocable);

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 if (now >= end) {
return token.balanceOf(this);
} 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);
}
}
}
64 changes: 64 additions & 0 deletions test/TokenVesting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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, true, { 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 linearly release tokens during vesting period');

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);
});

it('should fail to be revoked by owner if revocable not set');

it('should be emptied when revoked by owner');

});