From ec6e728c0f99947ccc9b0094d2e974bba527bd38 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Wed, 8 Feb 2017 12:31:20 +0100 Subject: [PATCH 01/12] Grantable Token first version --- contracts/token/GrantableToken.sol | 109 +++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 contracts/token/GrantableToken.sol diff --git a/contracts/token/GrantableToken.sol b/contracts/token/GrantableToken.sol new file mode 100644 index 000000000..9cce5813b --- /dev/null +++ b/contracts/token/GrantableToken.sol @@ -0,0 +1,109 @@ +pragma solidity ^0.4.8; + +import "./StandardToken.sol"; + +contract GrantableToken is StandardToken { + struct StockGrant { + address granter; + uint256 value; + uint64 cliff; + uint64 vesting; + uint64 start; + } + + mapping (address => StockGrant[]) public grants; + + function grantStock(address _to, uint256 _value) { + transfer(_to, _value); + } + + function grantVestedStock(address _to, uint256 _value, uint64 _start, uint64 _cliff, uint64 _vesting) { + if (_cliff < _start) throw; + if (_vesting < _start) throw; + if (_vesting < _cliff) throw; + + StockGrant memory grant = StockGrant({start: _start, value: _value, cliff: _cliff, vesting: _vesting, granter: msg.sender}); + grants[_to].push(grant); + + grantStock(_to, _value); + } + + function removeStockGrant(address _holder, uint _grantId) { + StockGrant grant = grants[_holder][_grantId]; + + if (grant.granter != msg.sender) throw; + uint256 nonVested = nonVestedShares(grant, uint64(now)); + + // remove grant from array + delete grants[_holder][_grantId]; + grants[_holder][_grantId] = grants[_holder][grants[_holder].length - 1]; + grants[_holder].length -= 1; + + balances[msg.sender] = safeAdd(balances[msg.sender], nonVested); + balances[_holder] = safeSub(balances[_holder], nonVested); + } + + function stockGrantCount(address _holder) constant returns (uint index) { + return grants[_holder].length; + } + + function stockGrant(address _holder, uint _grantId) constant returns (address granter, uint256 value, uint256 vested, uint64 start, uint64 cliff, uint64 vesting) { + StockGrant grant = grants[_holder][_grantId]; + + granter = grant.granter; + value = grant.value; + start = grant.start; + cliff = grant.cliff; + vesting = grant.vesting; + + vested = vestedShares(grant, uint64(now)); + } + + function vestedShares(StockGrant grant, uint64 time) private constant returns (uint256 vestedShares) { + if (time < grant.cliff) return 0; + if (time > grant.vesting) return grant.value; + + uint256 cliffShares = grant.value * uint256(grant.cliff - grant.start) / uint256(grant.vesting - grant.start); + vestedShares = cliffShares; + + uint256 vestingShares = safeSub(grant.value, cliffShares); + + vestedShares = safeAdd(vestedShares, vestingShares * (time - uint256(grant.cliff)) / uint256(grant.vesting - grant.start)); + } + + function nonVestedShares(StockGrant grant, uint64 time) private constant returns (uint256) { + return safeSub(grant.value, vestedShares(grant, time)); + } + + function lastStockIsTransferrableEvent(address holder) constant public returns (uint64 date) { + date = uint64(now); + uint256 grantIndex = grants[holder].length; + for (uint256 i = 0; i < grantIndex; i++) { + date = max64(grants[holder][i].vesting, date); + } + } + + function transferrableShares(address holder, uint64 time) constant public returns (uint256 nonVested) { + uint256 grantIndex = grants[holder].length; + + for (uint256 i = 0; i < grantIndex; i++) { + nonVested = safeAdd(nonVested, nonVestedShares(grants[holder][i], time)); + } + + return safeSub(balances[holder], nonVested); + } + + function transfer(address _to, uint _value) returns (bool success){ + if (_value > transferrableShares(msg.sender, uint64(now))) throw; + + return super.transfer(_to, _value); + } + + function max64(uint64 a, uint64 b) private constant returns (uint64) { + return a >= b ? a : b; + } + + function min256(uint256 a, uint256 b) private constant returns (uint256) { + return a < b ? a : b; + } +} From fb0a96332c9f553605038dcbe73062ab46d05844 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Wed, 8 Feb 2017 15:27:39 +0100 Subject: [PATCH 02/12] GrantableTokenMock --- contracts/test-helpers/GrantableTokenMock.sol | 7 ++ contracts/token/GrantableToken.sol | 2 +- test/AGrantableToken.js | 66 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 contracts/test-helpers/GrantableTokenMock.sol create mode 100644 test/AGrantableToken.js diff --git a/contracts/test-helpers/GrantableTokenMock.sol b/contracts/test-helpers/GrantableTokenMock.sol new file mode 100644 index 000000000..3f663caf1 --- /dev/null +++ b/contracts/test-helpers/GrantableTokenMock.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.4.4; +import "./StandardTokenMock.sol"; + +contract GrantableTokenMock is StandardTokenMock { + function GrantableTokenMock(address initialAccount, uint initialBalance) + StandardTokenMock(initialAccount, initialBalance) {} +} diff --git a/contracts/token/GrantableToken.sol b/contracts/token/GrantableToken.sol index 9cce5813b..45153bde9 100644 --- a/contracts/token/GrantableToken.sol +++ b/contracts/token/GrantableToken.sol @@ -28,7 +28,7 @@ contract GrantableToken is StandardToken { grantStock(_to, _value); } - function removeStockGrant(address _holder, uint _grantId) { + function revokeStockGrant(address _holder, uint _grantId) { StockGrant grant = grants[_holder][_grantId]; if (grant.granter != msg.sender) throw; diff --git a/test/AGrantableToken.js b/test/AGrantableToken.js new file mode 100644 index 000000000..0bfbd5421 --- /dev/null +++ b/test/AGrantableToken.js @@ -0,0 +1,66 @@ +const assertJump = require('./helpers/assertJump'); + +contract('GrantableToken', function(accounts) { + + it("should return the correct totalSupply after construction", async function() { + let token = await StandardTokenMock.new(accounts[0], 100); + let totalSupply = await token.totalSupply(); + + assert.equal(totalSupply, 100); + }) + + it("should return the correct allowance amount after approval", async function() { + let token = await StandardTokenMock.new(); + let approve = await token.approve(accounts[1], 100); + let allowance = await token.allowance(accounts[0], accounts[1]); + + assert.equal(allowance, 100); + }); + + it("should return correct balances after transfer", async function() { + let token = await StandardTokenMock.new(accounts[0], 100); + let transfer = await token.transfer(accounts[1], 100); + let balance0 = await token.balanceOf(accounts[0]); + assert.equal(balance0, 0); + + let balance1 = await token.balanceOf(accounts[1]); + assert.equal(balance1, 100); + }); + + it("should throw an error when trying to transfer more than balance", async function() { + let token = await StandardTokenMock.new(accounts[0], 100); + try { + let transfer = await token.transfer(accounts[1], 101); + } catch(error) { + return assertJump(error); + } + assert.fail('should have thrown before'); + }); + + it("should return correct balances after transfering from another account", async function() { + let token = await StandardTokenMock.new(accounts[0], 100); + let approve = await token.approve(accounts[1], 100); + let transferFrom = await token.transferFrom(accounts[0], accounts[2], 100, {from: accounts[1]}); + + let balance0 = await token.balanceOf(accounts[0]); + assert.equal(balance0, 0); + + let balance1 = await token.balanceOf(accounts[2]); + assert.equal(balance1, 100); + + let balance2 = await token.balanceOf(accounts[1]); + assert.equal(balance2, 0); + }); + + it("should throw an error when trying to transfer more than allowed", async function() { + let token = await StandardTokenMock.new(); + let approve = await token.approve(accounts[1], 99); + try { + let transfer = await token.transferFrom(accounts[0], accounts[2], 100, {from: accounts[1]}); + } catch (error) { + return assertJump(error); + } + assert.fail('should have thrown before'); + }); + +}); From 8d9e12eda38a3b35ffca5d848aefc8bf823e283d Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Thu, 9 Feb 2017 13:23:12 +0100 Subject: [PATCH 03/12] Add GrantableToken tests --- contracts/test-helpers/GrantableTokenMock.sol | 12 ++- contracts/token/GrantableToken.sol | 48 +++++------ package.json | 1 + test/AGrantableToken.js | 66 --------------- test/GrantableToken.js | 81 +++++++++++++++++++ test/helpers/timer.js | 5 ++ 6 files changed, 119 insertions(+), 94 deletions(-) delete mode 100644 test/AGrantableToken.js create mode 100644 test/GrantableToken.js create mode 100644 test/helpers/timer.js diff --git a/contracts/test-helpers/GrantableTokenMock.sol b/contracts/test-helpers/GrantableTokenMock.sol index 3f663caf1..74cadd1d1 100644 --- a/contracts/test-helpers/GrantableTokenMock.sol +++ b/contracts/test-helpers/GrantableTokenMock.sol @@ -1,7 +1,11 @@ pragma solidity ^0.4.4; -import "./StandardTokenMock.sol"; -contract GrantableTokenMock is StandardTokenMock { - function GrantableTokenMock(address initialAccount, uint initialBalance) - StandardTokenMock(initialAccount, initialBalance) {} +import '../token/GrantableToken.sol'; + +// mock class using StandardToken +contract GrantableTokenMock is GrantableToken { + function GrantableTokenMock(address initialAccount, uint initialBalance) { + balances[initialAccount] = initialBalance; + totalSupply = initialBalance; + } } diff --git a/contracts/token/GrantableToken.sol b/contracts/token/GrantableToken.sol index 45153bde9..9f2111b8d 100644 --- a/contracts/token/GrantableToken.sol +++ b/contracts/token/GrantableToken.sol @@ -3,7 +3,7 @@ pragma solidity ^0.4.8; import "./StandardToken.sol"; contract GrantableToken is StandardToken { - struct StockGrant { + struct TokenGrant { address granter; uint256 value; uint64 cliff; @@ -11,28 +11,28 @@ contract GrantableToken is StandardToken { uint64 start; } - mapping (address => StockGrant[]) public grants; + mapping (address => TokenGrant[]) public grants; - function grantStock(address _to, uint256 _value) { + function grantTokens(address _to, uint256 _value) { transfer(_to, _value); } - function grantVestedStock(address _to, uint256 _value, uint64 _start, uint64 _cliff, uint64 _vesting) { + function grantVestedTokens(address _to, uint256 _value, uint64 _start, uint64 _cliff, uint64 _vesting) { if (_cliff < _start) throw; if (_vesting < _start) throw; if (_vesting < _cliff) throw; - StockGrant memory grant = StockGrant({start: _start, value: _value, cliff: _cliff, vesting: _vesting, granter: msg.sender}); + TokenGrant memory grant = TokenGrant({start: _start, value: _value, cliff: _cliff, vesting: _vesting, granter: msg.sender}); grants[_to].push(grant); - grantStock(_to, _value); + grantTokens(_to, _value); } - function revokeStockGrant(address _holder, uint _grantId) { - StockGrant grant = grants[_holder][_grantId]; + function revokeTokenGrant(address _holder, uint _grantId) { + TokenGrant grant = grants[_holder][_grantId]; if (grant.granter != msg.sender) throw; - uint256 nonVested = nonVestedShares(grant, uint64(now)); + uint256 nonVested = nonVestedTokens(grant, uint64(now)); // remove grant from array delete grants[_holder][_grantId]; @@ -43,12 +43,12 @@ contract GrantableToken is StandardToken { balances[_holder] = safeSub(balances[_holder], nonVested); } - function stockGrantCount(address _holder) constant returns (uint index) { + function tokenGrantsCount(address _holder) constant returns (uint index) { return grants[_holder].length; } - function stockGrant(address _holder, uint _grantId) constant returns (address granter, uint256 value, uint256 vested, uint64 start, uint64 cliff, uint64 vesting) { - StockGrant grant = grants[_holder][_grantId]; + function tokenGrant(address _holder, uint _grantId) constant returns (address granter, uint256 value, uint256 vested, uint64 start, uint64 cliff, uint64 vesting) { + TokenGrant grant = grants[_holder][_grantId]; granter = grant.granter; value = grant.value; @@ -56,26 +56,26 @@ contract GrantableToken is StandardToken { cliff = grant.cliff; vesting = grant.vesting; - vested = vestedShares(grant, uint64(now)); + vested = vestedTokens(grant, uint64(now)); } - function vestedShares(StockGrant grant, uint64 time) private constant returns (uint256 vestedShares) { + function vestedTokens(TokenGrant grant, uint64 time) private constant returns (uint256 vestedTokens) { if (time < grant.cliff) return 0; if (time > grant.vesting) return grant.value; - uint256 cliffShares = grant.value * uint256(grant.cliff - grant.start) / uint256(grant.vesting - grant.start); - vestedShares = cliffShares; + uint256 cliffTokens = grant.value * uint256(grant.cliff - grant.start) / uint256(grant.vesting - grant.start); + vestedTokens = cliffTokens; - uint256 vestingShares = safeSub(grant.value, cliffShares); + uint256 vestingTokens = safeSub(grant.value, cliffTokens); - vestedShares = safeAdd(vestedShares, vestingShares * (time - uint256(grant.cliff)) / uint256(grant.vesting - grant.start)); + vestedTokens = safeAdd(vestedTokens, vestingTokens * (time - uint256(grant.cliff)) / uint256(grant.vesting - grant.start)); } - function nonVestedShares(StockGrant grant, uint64 time) private constant returns (uint256) { - return safeSub(grant.value, vestedShares(grant, time)); + function nonVestedTokens(TokenGrant grant, uint64 time) private constant returns (uint256) { + return safeSub(grant.value, vestedTokens(grant, time)); } - function lastStockIsTransferrableEvent(address holder) constant public returns (uint64 date) { + function lastTokenIsTransferrableEvent(address holder) constant public returns (uint64 date) { date = uint64(now); uint256 grantIndex = grants[holder].length; for (uint256 i = 0; i < grantIndex; i++) { @@ -83,18 +83,18 @@ contract GrantableToken is StandardToken { } } - function transferrableShares(address holder, uint64 time) constant public returns (uint256 nonVested) { + function transferrableTokens(address holder, uint64 time) constant public returns (uint256 nonVested) { uint256 grantIndex = grants[holder].length; for (uint256 i = 0; i < grantIndex; i++) { - nonVested = safeAdd(nonVested, nonVestedShares(grants[holder][i], time)); + nonVested = safeAdd(nonVested, nonVestedTokens(grants[holder][i], time)); } return safeSub(balances[holder], nonVested); } function transfer(address _to, uint _value) returns (bool success){ - if (_value > transferrableShares(msg.sender, uint64(now))) throw; + if (_value > transferrableTokens(msg.sender, uint64(now))) throw; return super.transfer(_to, _value); } diff --git a/package.json b/package.json index 91a2eb65d..24e5ff5dd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "scripts": { "test": "truffle test", + "console": "truffle console", "install": "scripts/install.sh" }, "repository": { diff --git a/test/AGrantableToken.js b/test/AGrantableToken.js deleted file mode 100644 index 0bfbd5421..000000000 --- a/test/AGrantableToken.js +++ /dev/null @@ -1,66 +0,0 @@ -const assertJump = require('./helpers/assertJump'); - -contract('GrantableToken', function(accounts) { - - it("should return the correct totalSupply after construction", async function() { - let token = await StandardTokenMock.new(accounts[0], 100); - let totalSupply = await token.totalSupply(); - - assert.equal(totalSupply, 100); - }) - - it("should return the correct allowance amount after approval", async function() { - let token = await StandardTokenMock.new(); - let approve = await token.approve(accounts[1], 100); - let allowance = await token.allowance(accounts[0], accounts[1]); - - assert.equal(allowance, 100); - }); - - it("should return correct balances after transfer", async function() { - let token = await StandardTokenMock.new(accounts[0], 100); - let transfer = await token.transfer(accounts[1], 100); - let balance0 = await token.balanceOf(accounts[0]); - assert.equal(balance0, 0); - - let balance1 = await token.balanceOf(accounts[1]); - assert.equal(balance1, 100); - }); - - it("should throw an error when trying to transfer more than balance", async function() { - let token = await StandardTokenMock.new(accounts[0], 100); - try { - let transfer = await token.transfer(accounts[1], 101); - } catch(error) { - return assertJump(error); - } - assert.fail('should have thrown before'); - }); - - it("should return correct balances after transfering from another account", async function() { - let token = await StandardTokenMock.new(accounts[0], 100); - let approve = await token.approve(accounts[1], 100); - let transferFrom = await token.transferFrom(accounts[0], accounts[2], 100, {from: accounts[1]}); - - let balance0 = await token.balanceOf(accounts[0]); - assert.equal(balance0, 0); - - let balance1 = await token.balanceOf(accounts[2]); - assert.equal(balance1, 100); - - let balance2 = await token.balanceOf(accounts[1]); - assert.equal(balance2, 0); - }); - - it("should throw an error when trying to transfer more than allowed", async function() { - let token = await StandardTokenMock.new(); - let approve = await token.approve(accounts[1], 99); - try { - let transfer = await token.transferFrom(accounts[0], accounts[2], 100, {from: accounts[1]}); - } catch (error) { - return assertJump(error); - } - assert.fail('should have thrown before'); - }); - -}); diff --git a/test/GrantableToken.js b/test/GrantableToken.js new file mode 100644 index 000000000..1f423e116 --- /dev/null +++ b/test/GrantableToken.js @@ -0,0 +1,81 @@ +const assertJump = require('./helpers/assertJump'); +const timer = require('./helpers/timer'); + +contract('GrantableToken', function(accounts) { + let token = null + let now = 0 + + const tokenAmount = 50 + + const granter = accounts[0] + const receiver = accounts[1] + + beforeEach(async () => { + token = await GrantableTokenMock.new(granter, 100); + now = +new Date()/1000; + }) + + it('granter can grant tokens without vesting', async () => { + await token.grantTokens(receiver, tokenAmount, { from: granter }) + + assert.equal(await token.balanceOf(receiver), tokenAmount); + assert.equal(await token.transferrableTokens(receiver, +new Date()/1000), tokenAmount); + }) + + describe('getting a token grant', async () => { + const cliff = 1 + const vesting = 2 // seconds + + beforeEach(async () => { + await token.grantVestedTokens(receiver, tokenAmount, now, now + cliff, now + vesting, { from: granter }) + }) + + it('tokens are received', async () => { + assert.equal(await token.balanceOf(receiver), tokenAmount); + }) + + it('has 0 transferrable tokens before cliff', async () => { + assert.equal(await token.transferrableTokens(receiver, now), 0); + }) + + it('all tokens are transferrable after vesting', async () => { + assert.equal(await token.transferrableTokens(receiver, now + vesting + 1), tokenAmount); + }) + + it('throws when trying to transfer non vested tokens', async () => { + try { + await token.transfer(accounts[7], 1, { from: receiver }) + } catch(error) { + return assertJump(error); + } + assert.fail('should have thrown before'); + }) + + it('can be revoked by granter', async () => { + await token.revokeTokenGrant(receiver, 0, { from: granter }); + assert.equal(await token.balanceOf(receiver), 0); + assert.equal(await token.balanceOf(granter), 100); + }) + + it('cannot be revoked by non granter', async () => { + try { + await token.revokeTokenGrant(receiver, 0, { from: accounts[3] }); + } catch(error) { + return assertJump(error); + } + assert.fail('should have thrown before'); + }) + + it('can be revoked by granter and non vested tokens are returned', async () => { + await timer(cliff); + await token.revokeTokenGrant(receiver, 0, { from: granter }); + assert.equal(await token.balanceOf(receiver), tokenAmount * cliff / vesting); + }) + + it('can transfer all tokens after vesting ends', async () => { + await timer(vesting + 1); + await token.transfer(accounts[7], tokenAmount, { from: receiver }) + assert.equal(await token.balanceOf(accounts[7]), tokenAmount); + }) + }) +}); diff --git a/test/helpers/timer.js b/test/helpers/timer.js new file mode 100644 index 000000000..ca969bcd0 --- /dev/null +++ b/test/helpers/timer.js @@ -0,0 +1,5 @@ +module.exports = s => { + return new Promise(resolve => { + setTimeout(() => resolve(), s * 1000 + 600) // 600ms breathing room for testrpc to sync + }) +} From 93e7984c61893d9689501e5cc7f7adbd9ccce63e Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 10 Feb 2017 14:59:19 +0100 Subject: [PATCH 04/12] Transferrable is now transferrable --- contracts/token/GrantableToken.sol | 4 ++-- test/GrantableToken.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/token/GrantableToken.sol b/contracts/token/GrantableToken.sol index 9f2111b8d..a97c2ed62 100644 --- a/contracts/token/GrantableToken.sol +++ b/contracts/token/GrantableToken.sol @@ -83,7 +83,7 @@ contract GrantableToken is StandardToken { } } - function transferrableTokens(address holder, uint64 time) constant public returns (uint256 nonVested) { + function transferableTokens(address holder, uint64 time) constant public returns (uint256 nonVested) { uint256 grantIndex = grants[holder].length; for (uint256 i = 0; i < grantIndex; i++) { @@ -94,7 +94,7 @@ contract GrantableToken is StandardToken { } function transfer(address _to, uint _value) returns (bool success){ - if (_value > transferrableTokens(msg.sender, uint64(now))) throw; + if (_value > transferableTokens(msg.sender, uint64(now))) throw; return super.transfer(_to, _value); } diff --git a/test/GrantableToken.js b/test/GrantableToken.js index 1f423e116..8f7ee4ac7 100644 --- a/test/GrantableToken.js +++ b/test/GrantableToken.js @@ -19,7 +19,7 @@ contract('GrantableToken', function(accounts) { await token.grantTokens(receiver, tokenAmount, { from: granter }) assert.equal(await token.balanceOf(receiver), tokenAmount); - assert.equal(await token.transferrableTokens(receiver, +new Date()/1000), tokenAmount); + assert.equal(await token.transferableTokens(receiver, +new Date()/1000), tokenAmount); }) describe('getting a token grant', async () => { @@ -34,12 +34,12 @@ contract('GrantableToken', function(accounts) { assert.equal(await token.balanceOf(receiver), tokenAmount); }) - it('has 0 transferrable tokens before cliff', async () => { - assert.equal(await token.transferrableTokens(receiver, now), 0); + it('has 0 transferable tokens before cliff', async () => { + assert.equal(await token.transferableTokens(receiver, now), 0); }) - it('all tokens are transferrable after vesting', async () => { - assert.equal(await token.transferrableTokens(receiver, now + vesting + 1), tokenAmount); + it('all tokens are transferable after vesting', async () => { + assert.equal(await token.transferableTokens(receiver, now + vesting + 1), tokenAmount); }) it('throws when trying to transfer non vested tokens', async () => { From 1697518da8c99f9ed21004edf27b6c3520c60ec2 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 10 Feb 2017 15:28:20 +0100 Subject: [PATCH 05/12] Make vested stock calculation more testable --- contracts/SafeMath.sol | 1 - contracts/token/GrantableToken.sol | 16 ++++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/SafeMath.sol b/contracts/SafeMath.sol index 8568ddd91..e25589724 100644 --- a/contracts/SafeMath.sol +++ b/contracts/SafeMath.sol @@ -26,4 +26,3 @@ contract SafeMath { if (!assertion) throw; } } - diff --git a/contracts/token/GrantableToken.sol b/contracts/token/GrantableToken.sol index a97c2ed62..2802a7ee9 100644 --- a/contracts/token/GrantableToken.sol +++ b/contracts/token/GrantableToken.sol @@ -59,16 +59,20 @@ contract GrantableToken is StandardToken { vested = vestedTokens(grant, uint64(now)); } - function vestedTokens(TokenGrant grant, uint64 time) private constant returns (uint256 vestedTokens) { - if (time < grant.cliff) return 0; - if (time > grant.vesting) return grant.value; + function vestedTokens(TokenGrant grant, uint64 time) private constant returns (uint256) { + return calculateVestedTokens(grant.value, uint256(time), uint256(grant.start), uint256(grant.cliff), uint256(grant.vesting)); + } - uint256 cliffTokens = grant.value * uint256(grant.cliff - grant.start) / uint256(grant.vesting - grant.start); + function calculateVestedTokens(uint256 tokens, uint256 time, uint256 start, uint256 cliff, uint256 vesting) constant returns (uint256 vestedTokens) { + if (time < cliff) return 0; + if (time > vesting) return tokens; + + uint256 cliffTokens = safeMul(tokens, (cliff - start) / (vesting - start)); vestedTokens = cliffTokens; - uint256 vestingTokens = safeSub(grant.value, cliffTokens); + uint256 vestingTokens = safeSub(tokens, cliffTokens); - vestedTokens = safeAdd(vestedTokens, vestingTokens * (time - uint256(grant.cliff)) / uint256(grant.vesting - grant.start)); + vestedTokens = safeAdd(vestedTokens, vestingTokens * (time - cliff) / (vesting - start)); } function nonVestedTokens(TokenGrant grant, uint64 time) private constant returns (uint256) { From abc646a95cd9c035794a0c83946d367e5445543c Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 10 Feb 2017 16:04:58 +0100 Subject: [PATCH 06/12] lastTokenIsTransferableDate rename --- contracts/token/GrantableToken.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/GrantableToken.sol b/contracts/token/GrantableToken.sol index 2802a7ee9..712a53021 100644 --- a/contracts/token/GrantableToken.sol +++ b/contracts/token/GrantableToken.sol @@ -79,7 +79,7 @@ contract GrantableToken is StandardToken { return safeSub(grant.value, vestedTokens(grant, time)); } - function lastTokenIsTransferrableEvent(address holder) constant public returns (uint64 date) { + function lastTokenIsTransferableDate(address holder) constant public returns (uint64 date) { date = uint64(now); uint256 grantIndex = grants[holder].length; for (uint256 i = 0; i < grantIndex; i++) { From 917b129517020a19dcb9a20af320a57f87fdbf2f Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 10 Feb 2017 16:17:02 +0100 Subject: [PATCH 07/12] Add safeDiv and min/max functions to SafeMath --- contracts/SafeMath.sol | 23 +++++++++++++++++++++++ contracts/token/GrantableToken.sol | 12 ++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/contracts/SafeMath.sol b/contracts/SafeMath.sol index e25589724..fd084967d 100644 --- a/contracts/SafeMath.sol +++ b/contracts/SafeMath.sol @@ -11,6 +11,13 @@ contract SafeMath { return c; } + function safeDiv(uint a, uint b) internal returns (uint) { + assert(b > 0); + uint c = a / b; + assert(a == b * c + a % b); + return c; + } + function safeSub(uint a, uint b) internal returns (uint) { assert(b <= a); return a - b; @@ -22,6 +29,22 @@ contract SafeMath { return c; } + function max64(uint64 a, uint64 b) internal constant returns (uint64) { + return a >= b ? a : b; + } + + function min64(uint64 a, uint64 b) internal constant returns (uint64) { + return a < b ? a : b; + } + + function max256(uint256 a, uint256 b) internal constant returns (uint256) { + return a >= b ? a : b; + } + + function min256(uint256 a, uint256 b) internal constant returns (uint256) { + return a < b ? a : b; + } + function assert(bool assertion) internal { if (!assertion) throw; } diff --git a/contracts/token/GrantableToken.sol b/contracts/token/GrantableToken.sol index 712a53021..79fc328e9 100644 --- a/contracts/token/GrantableToken.sol +++ b/contracts/token/GrantableToken.sol @@ -67,12 +67,12 @@ contract GrantableToken is StandardToken { if (time < cliff) return 0; if (time > vesting) return tokens; - uint256 cliffTokens = safeMul(tokens, (cliff - start) / (vesting - start)); + uint256 cliffTokens = safeMul(tokens, safeDiv(safeSub(cliff, start), safeSub(vesting, start))); vestedTokens = cliffTokens; uint256 vestingTokens = safeSub(tokens, cliffTokens); - vestedTokens = safeAdd(vestedTokens, vestingTokens * (time - cliff) / (vesting - start)); + vestedTokens = safeAdd(vestedTokens, safeMul(vestingTokens, safeDiv(safeSub(time, cliff), safeSub(vesting, start)))); } function nonVestedTokens(TokenGrant grant, uint64 time) private constant returns (uint256) { @@ -102,12 +102,4 @@ contract GrantableToken is StandardToken { return super.transfer(_to, _value); } - - function max64(uint64 a, uint64 b) private constant returns (uint64) { - return a >= b ? a : b; - } - - function min256(uint256 a, uint256 b) private constant returns (uint256) { - return a < b ? a : b; - } } From f305382ef44a92c3689934358b0bc98c17db5424 Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 10 Feb 2017 16:18:36 +0100 Subject: [PATCH 08/12] Remove grantTokens without vesting function --- contracts/token/GrantableToken.sol | 6 +----- test/GrantableToken.js | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/contracts/token/GrantableToken.sol b/contracts/token/GrantableToken.sol index 79fc328e9..cf5e955c6 100644 --- a/contracts/token/GrantableToken.sol +++ b/contracts/token/GrantableToken.sol @@ -13,10 +13,6 @@ contract GrantableToken is StandardToken { mapping (address => TokenGrant[]) public grants; - function grantTokens(address _to, uint256 _value) { - transfer(_to, _value); - } - function grantVestedTokens(address _to, uint256 _value, uint64 _start, uint64 _cliff, uint64 _vesting) { if (_cliff < _start) throw; if (_vesting < _start) throw; @@ -25,7 +21,7 @@ contract GrantableToken is StandardToken { TokenGrant memory grant = TokenGrant({start: _start, value: _value, cliff: _cliff, vesting: _vesting, granter: msg.sender}); grants[_to].push(grant); - grantTokens(_to, _value); + transfer(_to, _value); } function revokeTokenGrant(address _holder, uint _grantId) { diff --git a/test/GrantableToken.js b/test/GrantableToken.js index 8f7ee4ac7..45bcfc05a 100644 --- a/test/GrantableToken.js +++ b/test/GrantableToken.js @@ -16,7 +16,7 @@ contract('GrantableToken', function(accounts) { }) it('granter can grant tokens without vesting', async () => { - await token.grantTokens(receiver, tokenAmount, { from: granter }) + await token.transfer(receiver, tokenAmount, { from: granter }) assert.equal(await token.balanceOf(receiver), tokenAmount); assert.equal(await token.transferableTokens(receiver, +new Date()/1000), tokenAmount); From 0b71dcded2719050c2385890fa5675ba03c274de Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Fri, 10 Feb 2017 16:20:23 +0100 Subject: [PATCH 09/12] Rename GrantableToken to VestedToken --- contracts/test-helpers/GrantableTokenMock.sol | 11 ----------- contracts/test-helpers/VestedTokenMock.sol | 11 +++++++++++ .../token/{GrantableToken.sol => VestedToken.sol} | 2 +- test/{GrantableToken.js => VestedToken.js} | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 contracts/test-helpers/GrantableTokenMock.sol create mode 100644 contracts/test-helpers/VestedTokenMock.sol rename contracts/token/{GrantableToken.sol => VestedToken.sol} (98%) rename test/{GrantableToken.js => VestedToken.js} (96%) diff --git a/contracts/test-helpers/GrantableTokenMock.sol b/contracts/test-helpers/GrantableTokenMock.sol deleted file mode 100644 index 74cadd1d1..000000000 --- a/contracts/test-helpers/GrantableTokenMock.sol +++ /dev/null @@ -1,11 +0,0 @@ -pragma solidity ^0.4.4; - -import '../token/GrantableToken.sol'; - -// mock class using StandardToken -contract GrantableTokenMock is GrantableToken { - function GrantableTokenMock(address initialAccount, uint initialBalance) { - balances[initialAccount] = initialBalance; - totalSupply = initialBalance; - } -} diff --git a/contracts/test-helpers/VestedTokenMock.sol b/contracts/test-helpers/VestedTokenMock.sol new file mode 100644 index 000000000..01ff87dd9 --- /dev/null +++ b/contracts/test-helpers/VestedTokenMock.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.4.4; + +import '../token/VestedToken.sol'; + +// mock class using StandardToken +contract VestedTokenMock is VestedToken { + function VestedTokenMock(address initialAccount, uint initialBalance) { + balances[initialAccount] = initialBalance; + totalSupply = initialBalance; + } +} diff --git a/contracts/token/GrantableToken.sol b/contracts/token/VestedToken.sol similarity index 98% rename from contracts/token/GrantableToken.sol rename to contracts/token/VestedToken.sol index cf5e955c6..20bea41b3 100644 --- a/contracts/token/GrantableToken.sol +++ b/contracts/token/VestedToken.sol @@ -2,7 +2,7 @@ pragma solidity ^0.4.8; import "./StandardToken.sol"; -contract GrantableToken is StandardToken { +contract VestedToken is StandardToken { struct TokenGrant { address granter; uint256 value; diff --git a/test/GrantableToken.js b/test/VestedToken.js similarity index 96% rename from test/GrantableToken.js rename to test/VestedToken.js index 45bcfc05a..fc72d0c2e 100644 --- a/test/GrantableToken.js +++ b/test/VestedToken.js @@ -1,7 +1,7 @@ const assertJump = require('./helpers/assertJump'); const timer = require('./helpers/timer'); -contract('GrantableToken', function(accounts) { +contract('VestedToken', function(accounts) { let token = null let now = 0 @@ -11,7 +11,7 @@ contract('GrantableToken', function(accounts) { const receiver = accounts[1] beforeEach(async () => { - token = await GrantableTokenMock.new(granter, 100); + token = await VestedTokenMock.new(granter, 100); now = +new Date()/1000; }) From b67f60929c20ef73046ac383c4af258bcb4f1668 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Fri, 10 Feb 2017 18:32:58 -0300 Subject: [PATCH 10/12] make VestedToken test deterministic --- contracts/token/VestedToken.sol | 2 +- test/VestedToken.js | 39 +++++++++++++++++++-------------- test/helpers/timer.js | 20 +++++++++++++---- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/contracts/token/VestedToken.sol b/contracts/token/VestedToken.sol index 20bea41b3..e42b5ff9c 100644 --- a/contracts/token/VestedToken.sol +++ b/contracts/token/VestedToken.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.8; +pragma solidity ^0.4.4; import "./StandardToken.sol"; diff --git a/test/VestedToken.js b/test/VestedToken.js index fc72d0c2e..c895f2cb2 100644 --- a/test/VestedToken.js +++ b/test/VestedToken.js @@ -2,29 +2,32 @@ const assertJump = require('./helpers/assertJump'); const timer = require('./helpers/timer'); contract('VestedToken', function(accounts) { - let token = null - let now = 0 + + let token = null; + let now = 0; - const tokenAmount = 50 + const tokenAmount = 50; - const granter = accounts[0] - const receiver = accounts[1] + const granter = accounts[3]; + const receiver = accounts[4]; beforeEach(async () => { - token = await VestedTokenMock.new(granter, 100); + token = await VestedTokenMock.new(granter, tokenAmount*2); now = +new Date()/1000; - }) + var block = await web3.eth.getBlock('latest'); + now = block.timestamp; + }); it('granter can grant tokens without vesting', async () => { - await token.transfer(receiver, tokenAmount, { from: granter }) + await token.transfer(receiver, tokenAmount, { from: granter }); assert.equal(await token.balanceOf(receiver), tokenAmount); assert.equal(await token.transferableTokens(receiver, +new Date()/1000), tokenAmount); }) describe('getting a token grant', async () => { - const cliff = 1 - const vesting = 2 // seconds + const cliff = 10; + const vesting = 20; // seconds beforeEach(async () => { await token.grantVestedTokens(receiver, tokenAmount, now, now + cliff, now + vesting, { from: granter }) @@ -44,7 +47,7 @@ contract('VestedToken', function(accounts) { it('throws when trying to transfer non vested tokens', async () => { try { - await token.transfer(accounts[7], 1, { from: receiver }) + await token.transfer(accounts[9], 1, { from: receiver }) } catch(error) { return assertJump(error); } @@ -59,23 +62,27 @@ contract('VestedToken', function(accounts) { it('cannot be revoked by non granter', async () => { try { - await token.revokeTokenGrant(receiver, 0, { from: accounts[3] }); + await token.revokeTokenGrant(receiver, 0, { from: accounts[9] }); } catch(error) { return assertJump(error); } assert.fail('should have thrown before'); }) - it('can be revoked by granter and non vested tokens are returned', async () => { + it.only('can be revoked by granter and non vested tokens are returned', async () => { await timer(cliff); await token.revokeTokenGrant(receiver, 0, { from: granter }); - assert.equal(await token.balanceOf(receiver), tokenAmount * cliff / vesting); + var balance = await token.balanceOf(receiver); + var expectedBalance = tokenAmount * cliff / vesting; + console.log('real balance', balance.toString()); + console.log('expected ', expectedBalance); + assert.equal(balance, expectedBalance); }) it('can transfer all tokens after vesting ends', async () => { await timer(vesting + 1); - await token.transfer(accounts[7], tokenAmount, { from: receiver }) - assert.equal(await token.balanceOf(accounts[7]), tokenAmount); + await token.transfer(accounts[9], tokenAmount, { from: receiver }) + assert.equal(await token.balanceOf(accounts[9]), tokenAmount); }) }) }); diff --git a/test/helpers/timer.js b/test/helpers/timer.js index ca969bcd0..032b2cfcc 100644 --- a/test/helpers/timer.js +++ b/test/helpers/timer.js @@ -1,5 +1,17 @@ +// timer for tests specific to testrpc module.exports = s => { - return new Promise(resolve => { - setTimeout(() => resolve(), s * 1000 + 600) // 600ms breathing room for testrpc to sync - }) -} + console.log('timer to', s); + return new Promise((resolve, reject) => { + web3.currentProvider.sendAsync({ + jsonrpc: '2.0', + method: 'evm_increaseTime', + params: [s], // 60 seaconds, may need to be hex, I forget + id: new Date().getTime() // Id of the request; anything works, really + }, function(err) { + console.log('resolved to', err); + if (err) return reject(err); + resolve(); + }); + //setTimeout(() => resolve(), s * 1000 + 600) // 600ms breathing room for testrpc to sync + }); +}; From ee56abcc8a981300b0f33c017cb1c67d65d40db0 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Fri, 10 Feb 2017 18:35:05 -0300 Subject: [PATCH 11/12] remove console.logs from timer --- test/helpers/timer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/helpers/timer.js b/test/helpers/timer.js index 032b2cfcc..4cd8f0916 100644 --- a/test/helpers/timer.js +++ b/test/helpers/timer.js @@ -1,6 +1,5 @@ // timer for tests specific to testrpc module.exports = s => { - console.log('timer to', s); return new Promise((resolve, reject) => { web3.currentProvider.sendAsync({ jsonrpc: '2.0', @@ -8,7 +7,6 @@ module.exports = s => { params: [s], // 60 seaconds, may need to be hex, I forget id: new Date().getTime() // Id of the request; anything works, really }, function(err) { - console.log('resolved to', err); if (err) return reject(err); resolve(); }); From 0a5af4b8ac929070aaff4efc10ca218c25d93eaa Mon Sep 17 00:00:00 2001 From: Jorge Izquierdo Date: Mon, 13 Feb 2017 11:35:32 +0100 Subject: [PATCH 12/12] Change operations order for rounding. Make tests use blocktime as reference time rather than date. --- contracts/token/VestedToken.sol | 7 +++--- test/VestedToken.js | 43 ++++++++++++++------------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/contracts/token/VestedToken.sol b/contracts/token/VestedToken.sol index e42b5ff9c..9aec2d282 100644 --- a/contracts/token/VestedToken.sol +++ b/contracts/token/VestedToken.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.4; +pragma solidity ^0.4.8; import "./StandardToken.sol"; @@ -37,6 +37,7 @@ contract VestedToken is StandardToken { balances[msg.sender] = safeAdd(balances[msg.sender], nonVested); balances[_holder] = safeSub(balances[_holder], nonVested); + Transfer(_holder, msg.sender, nonVested); } function tokenGrantsCount(address _holder) constant returns (uint index) { @@ -63,12 +64,12 @@ contract VestedToken is StandardToken { if (time < cliff) return 0; if (time > vesting) return tokens; - uint256 cliffTokens = safeMul(tokens, safeDiv(safeSub(cliff, start), safeSub(vesting, start))); + uint256 cliffTokens = safeDiv(safeMul(tokens, safeSub(cliff, start)), safeSub(vesting, start)); vestedTokens = cliffTokens; uint256 vestingTokens = safeSub(tokens, cliffTokens); - vestedTokens = safeAdd(vestedTokens, safeMul(vestingTokens, safeDiv(safeSub(time, cliff), safeSub(vesting, start)))); + vestedTokens = safeAdd(vestedTokens, safeDiv(safeMul(vestingTokens, safeSub(time, cliff)), safeSub(vesting, start))); } function nonVestedTokens(TokenGrant grant, uint64 time) private constant returns (uint256) { diff --git a/test/VestedToken.js b/test/VestedToken.js index c895f2cb2..ebd6a587d 100644 --- a/test/VestedToken.js +++ b/test/VestedToken.js @@ -2,32 +2,29 @@ const assertJump = require('./helpers/assertJump'); const timer = require('./helpers/timer'); contract('VestedToken', function(accounts) { - - let token = null; - let now = 0; + let token = null + let now = 0 - const tokenAmount = 50; + const tokenAmount = 50 - const granter = accounts[3]; - const receiver = accounts[4]; + const granter = accounts[0] + const receiver = accounts[1] beforeEach(async () => { - token = await VestedTokenMock.new(granter, tokenAmount*2); - now = +new Date()/1000; - var block = await web3.eth.getBlock('latest'); - now = block.timestamp; - }); + token = await VestedTokenMock.new(granter, 100); + now = web3.eth.getBlock(web3.eth.blockNumber).timestamp; + }) it('granter can grant tokens without vesting', async () => { - await token.transfer(receiver, tokenAmount, { from: granter }); + await token.transfer(receiver, tokenAmount, { from: granter }) assert.equal(await token.balanceOf(receiver), tokenAmount); - assert.equal(await token.transferableTokens(receiver, +new Date()/1000), tokenAmount); + assert.equal(await token.transferableTokens(receiver, now), tokenAmount); }) describe('getting a token grant', async () => { - const cliff = 10; - const vesting = 20; // seconds + const cliff = 10000 + const vesting = 20000 // seconds beforeEach(async () => { await token.grantVestedTokens(receiver, tokenAmount, now, now + cliff, now + vesting, { from: granter }) @@ -47,7 +44,7 @@ contract('VestedToken', function(accounts) { it('throws when trying to transfer non vested tokens', async () => { try { - await token.transfer(accounts[9], 1, { from: receiver }) + await token.transfer(accounts[7], 1, { from: receiver }) } catch(error) { return assertJump(error); } @@ -62,27 +59,23 @@ contract('VestedToken', function(accounts) { it('cannot be revoked by non granter', async () => { try { - await token.revokeTokenGrant(receiver, 0, { from: accounts[9] }); + await token.revokeTokenGrant(receiver, 0, { from: accounts[3] }); } catch(error) { return assertJump(error); } assert.fail('should have thrown before'); }) - it.only('can be revoked by granter and non vested tokens are returned', async () => { + it('can be revoked by granter and non vested tokens are returned', async () => { await timer(cliff); await token.revokeTokenGrant(receiver, 0, { from: granter }); - var balance = await token.balanceOf(receiver); - var expectedBalance = tokenAmount * cliff / vesting; - console.log('real balance', balance.toString()); - console.log('expected ', expectedBalance); - assert.equal(balance, expectedBalance); + assert.equal(await token.balanceOf(receiver), tokenAmount * cliff / vesting); }) it('can transfer all tokens after vesting ends', async () => { await timer(vesting + 1); - await token.transfer(accounts[9], tokenAmount, { from: receiver }) - assert.equal(await token.balanceOf(accounts[9]), tokenAmount); + await token.transfer(accounts[7], tokenAmount, { from: receiver }) + assert.equal(await token.balanceOf(accounts[7]), tokenAmount); }) }) });