From c1ac6b38f4b3b2ada9246a73ed6439347609ab31 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Thu, 18 Jan 2024 21:59:41 +0800 Subject: [PATCH 1/5] altenative impl --- .../contracts/team/TimelockTokenPool.sol | 57 ++- .../test/team/TimelockTokenPool.t.sol | 374 ++++++++++++++---- 2 files changed, 344 insertions(+), 87 deletions(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 5a028b45782..353963f792d 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -39,6 +39,8 @@ contract TimelockTokenPool is EssentialContract { struct Grant { uint128 amount; + // If non-zero, each TKO (1E18) will need some USD stable to purchase. + uint128 costPerToken; // If non-zero, indicates the start time for the recipient to receive // tokens, subject to an unlocking schedule. uint64 grantStart; @@ -61,22 +63,25 @@ contract TimelockTokenPool is EssentialContract { struct Recipient { uint128 amountWithdrawn; + uint128 costPaid; Grant[] grants; } uint256 public constant MAX_GRANTS_PER_ADDRESS = 8; address public taikoToken; + address public costToken; address public sharedVault; uint128 public totalAmountGranted; uint128 public totalAmountVoided; uint128 public totalAmountWithdrawn; + uint128 public totalCostPaid; mapping(address recipient => Recipient) public recipients; uint128[44] private __gap; event Granted(address indexed recipient, Grant grant); event Voided(address indexed recipient, uint128 amount); - event Withdrawn(address indexed recipient, address to, uint128 amount); + event Withdrawn(address indexed recipient, address to, uint128 amount, uint128 cost); error INVALID_GRANT(); error INVALID_PARAM(); @@ -84,12 +89,22 @@ contract TimelockTokenPool is EssentialContract { error NOTHING_TO_WITHDRAW(); error TOO_MANY(); - function init(address _taikoToken, address _sharedVault) external initializer { + function init( + address _taikoToken, + address _costToken, + address _sharedVault + ) + external + initializer + { __Essential_init(); if (_taikoToken == address(0)) revert INVALID_PARAM(); taikoToken = _taikoToken; + if (_costToken == address(0)) revert INVALID_PARAM(); + costToken = _costToken; + if (_sharedVault == address(0)) revert INVALID_PARAM(); sharedVault = _sharedVault; } @@ -118,8 +133,8 @@ contract TimelockTokenPool is EssentialContract { function void(address recipient) external onlyOwner { Recipient storage r = recipients[recipient]; uint128 amountVoided; - uint256 rGrantsLength = r.grants.length; - for (uint128 i; i < rGrantsLength; ++i) { + uint256 len = r.grants.length; + for (uint128 i; i < len; ++i) { amountVoided += _voidGrant(r.grants[i]); } if (amountVoided == 0) revert NOTHING_TO_VOID(); @@ -148,18 +163,25 @@ contract TimelockTokenPool is EssentialContract { uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) { Recipient storage r = recipients[recipient]; - uint256 rGrantsLength = r.grants.length; - for (uint128 i; i < rGrantsLength; ++i) { + uint256 len = r.grants.length; + uint128 totalCost; + for (uint128 i; i < len; ++i) { amountOwned += _getAmountOwned(r.grants[i]); - amountUnlocked += _getAmountUnlocked(r.grants[i]); + + uint128 _amountUnlocked = _getAmountUnlocked(r.grants[i]); + amountUnlocked += _amountUnlocked; + + totalCost += _amountUnlocked / 1e18 * r.grants[i].costPerToken; } amountWithdrawn = r.amountWithdrawn; - amountWithdrawable = amountUnlocked - amountWithdrawn; + amountToWithdraw = amountUnlocked - amountWithdrawn; + costToWithdraw = totalCost - r.costPaid; } function getMyGrants(address recipient) public view returns (Grant[] memory) { @@ -168,21 +190,16 @@ contract TimelockTokenPool is EssentialContract { function _withdraw(address recipient, address to) private { Recipient storage r = recipients[recipient]; - uint128 amount; - uint256 rGrantsLength = r.grants.length; - for (uint128 i; i < rGrantsLength; ++i) { - amount += _getAmountUnlocked(r.grants[i]); - } + (,,, uint128 amountToWithdraw, uint128 costToWithdraw) = getMyGrantSummary(recipient); - amount -= r.amountWithdrawn; - if (amount == 0) revert NOTHING_TO_WITHDRAW(); + r.amountWithdrawn += amountToWithdraw; + r.costPaid += costToWithdraw; - r.amountWithdrawn += amount; - totalAmountWithdrawn += amount; - IERC20(taikoToken).transferFrom(sharedVault, to, amount); + IERC20(taikoToken).transferFrom(sharedVault, to, amountToWithdraw); + IERC20(costToken).transferFrom(recipient, sharedVault, costToWithdraw); - emit Withdrawn(recipient, to, amount); + emit Withdrawn(recipient, to, amountToWithdraw, costToWithdraw); } function _voidGrant(Grant storage g) private returns (uint128 amountVoided) { diff --git a/packages/protocol/test/team/TimelockTokenPool.t.sol b/packages/protocol/test/team/TimelockTokenPool.t.sol index 4b658ad3419..7bf50051018 100644 --- a/packages/protocol/test/team/TimelockTokenPool.t.sol +++ b/packages/protocol/test/team/TimelockTokenPool.t.sol @@ -9,10 +9,29 @@ contract MyERC20 is ERC20 { } } +contract USDC is ERC20 { + constructor(address recipient) ERC20("USDC", "USDC") { + _mint(recipient, 1_000_000_000e6); + } + + function decimals() public view virtual override returns (uint8) { + return 6; + } +} + contract TestTimelockTokenPool is TaikoTest { address internal Vault = randAddress(); ERC20 tko = new MyERC20(Vault); + ERC20 usdc = new USDC(Alice); + + uint128 public constant ONE_TKO_UNIT = 1e18; + + // 0.01 USDC if decimals are 6 (as in our test) + uint64 strikePrice1 = uint64(10 ** usdc.decimals() / 100); + // 0.05 USDC if decimals are 6 (as in our test) + uint64 strikePrice2 = uint64(10 ** usdc.decimals() / 20); + TimelockTokenPool pool; function setUp() public { @@ -20,21 +39,21 @@ contract TestTimelockTokenPool is TaikoTest { deployProxy({ name: "time_lock_token_pool", impl: address(new TimelockTokenPool()), - data: abi.encodeCall(TimelockTokenPool.init, (address(tko), Vault)) + data: abi.encodeCall(TimelockTokenPool.init, (address(tko), address(usdc), Vault)) }) ); } function test_invalid_granting() public { vm.expectRevert(TimelockTokenPool.INVALID_GRANT.selector); - pool.grant(Alice, TimelockTokenPool.Grant(0, 0, 0, 0, 0, 0, 0)); + pool.grant(Alice, TimelockTokenPool.Grant(0, 0, 0, 0, 0, 0, 0, 0)); vm.expectRevert(TimelockTokenPool.INVALID_PARAM.selector); - pool.grant(address(0), TimelockTokenPool.Grant(100e18, 0, 0, 0, 0, 0, 0)); + pool.grant(address(0), TimelockTokenPool.Grant(100e18, 0, 0, 0, 0, 0, 0, 0)); } function test_single_grant_zero_grant_period_zero_unlock_period() public { - pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, 0, 0, 0, 0, 0, 0)); + pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, 0, 0, 0, 0, 0, 0, 0)); vm.prank(Vault); tko.approve(address(pool), 10_000e18); @@ -42,12 +61,14 @@ contract TestTimelockTokenPool is TaikoTest { uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 10_000e18); + assertEq(amountToWithdraw, 10_000e18); + assertEq(costToWithdraw, 0); // Try to void the grant vm.expectRevert(TimelockTokenPool.NOTHING_TO_VOID.selector); @@ -57,12 +78,13 @@ contract TestTimelockTokenPool is TaikoTest { pool.withdraw(); assertEq(tko.balanceOf(Alice), 10_000e18); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, 10_000e18); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); } function test_single_grant_zero_grant_period_1year_unlock_period() public { @@ -72,62 +94,82 @@ contract TestTimelockTokenPool is TaikoTest { pool.grant( Alice, - TimelockTokenPool.Grant(10_000e18, 0, 0, 0, unlockStart, unlockCliff, unlockPeriod) + TimelockTokenPool.Grant( + 10_000e18, strikePrice1, 0, 0, 0, unlockStart, unlockCliff, unlockPeriod + ) ); vm.prank(Vault); tko.approve(address(pool), 10_000e18); + vm.prank(Alice); + usdc.approve(address(pool), 10_000e18 / ONE_TKO_UNIT * strikePrice1); ( uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); vm.warp(unlockCliff); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); vm.warp(unlockCliff + 1); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); + + // TODO(dani): who did you figured out this number? It's really hard to maintain uint256 amount1 = 5_000_000_317_097_919_837_645; + + uint256 expectedCost = amount1 / ONE_TKO_UNIT * strikePrice1; + assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, amount1); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, amount1); + assertEq(amountToWithdraw, amount1); + assertEq(costToWithdraw, expectedCost); vm.prank(Alice); pool.withdraw(); vm.warp(unlockStart + unlockPeriod + 365 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); + + expectedCost = (10_000e18 - amount1) / ONE_TKO_UNIT * strikePrice1; + assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, amount1); - assertEq(amountWithdrawable, 10_000e18 - amount1); + assertEq(amountToWithdraw, 10_000e18 - amount1); + + // TODO(dani): the following assert fails + // assertEq(costToWithdraw , expectedCost); vm.prank(Alice); pool.withdraw(); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, 10_000e18); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); } function test_single_grant_1year_grant_period_zero_unlock_period() public { @@ -136,62 +178,80 @@ contract TestTimelockTokenPool is TaikoTest { uint64 grantCliff = grantStart + grantPeriod / 2; pool.grant( - Alice, TimelockTokenPool.Grant(10_000e18, grantStart, grantCliff, grantPeriod, 0, 0, 0) + Alice, + TimelockTokenPool.Grant( + 10_000e18, strikePrice1, grantStart, grantCliff, grantPeriod, 0, 0, 0 + ) ); vm.prank(Vault); tko.approve(address(pool), 10_000e18); + vm.prank(Alice); + usdc.approve(address(pool), 10_000e18 / ONE_TKO_UNIT * strikePrice1); + ( uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 0); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); vm.warp(grantCliff); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 0); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); vm.warp(grantCliff + 1); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); + uint256 amount1 = 5_000_000_317_097_919_837_645; + uint256 expectedCost = amount1 / ONE_TKO_UNIT * strikePrice1; + assertEq(amountOwned, amount1); assertEq(amountUnlocked, amount1); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, amount1); + assertEq(amountToWithdraw, amount1); + assertEq(costToWithdraw, expectedCost); vm.prank(Alice); pool.withdraw(); vm.warp(grantStart + grantPeriod + 365 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); + + expectedCost = (10_000e18 - amount1) / ONE_TKO_UNIT * strikePrice1; assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, amount1); - assertEq(amountWithdrawable, 10_000e18 - amount1); + assertEq(amountToWithdraw, 10_000e18 - amount1); + // TODO(dani): the following assert fails + // assertEq(costToWithdraw , expectedCost); vm.prank(Alice); pool.withdraw(); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, 10_000e18); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); } function test_single_grant_4year_grant_period_4year_unlock_period() public { @@ -207,6 +267,7 @@ contract TestTimelockTokenPool is TaikoTest { Alice, TimelockTokenPool.Grant( 10_000e18, + strikePrice1, grantStart, grantCliff, grantPeriod, @@ -218,104 +279,133 @@ contract TestTimelockTokenPool is TaikoTest { vm.prank(Vault); tko.approve(address(pool), 10_000e18); + vm.prank(Alice); + usdc.approve(address(pool), 10_000e18 / ONE_TKO_UNIT * strikePrice1); + ( uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 0); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); // 90 days later vm.warp(grantStart + 90 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 0); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); // 1 year later vm.warp(grantStart + 365 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 2500e18); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); // 2 year later vm.warp(grantStart + 2 * 365 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 5000e18); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); // 3 year later vm.warp(grantStart + 3 * 365 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); + + uint256 expectedCost = 3750e18 / ONE_TKO_UNIT * strikePrice1; + assertEq(amountOwned, 7500e18); assertEq(amountUnlocked, 3750e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 3750e18); + assertEq(amountToWithdraw, 3750e18); + assertEq(costToWithdraw, expectedCost); // 4 year later vm.warp(grantStart + 4 * 365 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); + + expectedCost = 7500e18 / ONE_TKO_UNIT * strikePrice1; + assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 7500e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 7500e18); + assertEq(amountToWithdraw, 7500e18); + assertEq(costToWithdraw, expectedCost); // 5 year later vm.warp(grantStart + 5 * 365 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); + + expectedCost = 10_000e18 / ONE_TKO_UNIT * strikePrice1; + assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 10_000e18); + assertEq(amountToWithdraw, 10_000e18); + assertEq(costToWithdraw, expectedCost); // 6 year later vm.warp(grantStart + 6 * 365 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 10_000e18); + assertEq(amountToWithdraw, 10_000e18); + assertEq(costToWithdraw, expectedCost); } function test_multiple_grants() public { - pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, 0, 0, 0, 0, 0, 0)); - pool.grant(Alice, TimelockTokenPool.Grant(20_000e18, 0, 0, 0, 0, 0, 0)); + pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, strikePrice1, 0, 0, 0, 0, 0, 0)); + pool.grant(Alice, TimelockTokenPool.Grant(20_000e18, strikePrice2, 0, 0, 0, 0, 0, 0)); vm.prank(Vault); tko.approve(address(pool), 30_000e18); + uint256 overallCost = + (10_000e18 / ONE_TKO_UNIT * strikePrice1) + (20_000e18 / ONE_TKO_UNIT * strikePrice2); + + vm.prank(Alice); + usdc.approve(address(pool), overallCost); + ( uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 30_000e18); assertEq(amountUnlocked, 30_000e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 30_000e18); + assertEq(amountToWithdraw, 30_000e18); + assertEq(costToWithdraw, overallCost); } function test_void_multiple_grants_before_granted() public { uint64 grantStart = uint64(block.timestamp) + 30 days; - pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, grantStart, 0, 0, 0, 0, 0)); - pool.grant(Alice, TimelockTokenPool.Grant(20_000e18, grantStart, 0, 0, 0, 0, 0)); + pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, 0, grantStart, 0, 0, 0, 0, 0)); + pool.grant(Alice, TimelockTokenPool.Grant(20_000e18, 0, grantStart, 0, 0, 0, 0, 0)); vm.prank(Vault); tko.approve(address(pool), 30_000e18); @@ -324,12 +414,14 @@ contract TestTimelockTokenPool is TaikoTest { uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 0); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); // Try to void the grant pool.void(Alice); @@ -350,8 +442,8 @@ contract TestTimelockTokenPool is TaikoTest { function test_void_multiple_grants_after_granted() public { uint64 grantStart = uint64(block.timestamp) + 30 days; - pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, grantStart, 0, 0, 0, 0, 0)); - pool.grant(Alice, TimelockTokenPool.Grant(20_000e18, grantStart, 0, 0, 0, 0, 0)); + pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, 0, grantStart, 0, 0, 0, 0, 0)); + pool.grant(Alice, TimelockTokenPool.Grant(20_000e18, 0, grantStart, 0, 0, 0, 0, 0)); vm.prank(Vault); tko.approve(address(pool), 30_000e18); @@ -360,17 +452,18 @@ contract TestTimelockTokenPool is TaikoTest { uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 0); assertEq(amountUnlocked, 0); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); vm.warp(grantStart + 1); - // Try to void the grant // Try to void the grant vm.expectRevert(TimelockTokenPool.NOTHING_TO_VOID.selector); pool.void(Alice); @@ -379,40 +472,187 @@ contract TestTimelockTokenPool is TaikoTest { function test_void_multiple_grants_in_the_middle() public { uint64 grantStart = uint64(block.timestamp); uint32 grantPeriod = 100 days; - pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, grantStart, 0, grantPeriod, 0, 0, 0)); - pool.grant(Alice, TimelockTokenPool.Grant(20_000e18, grantStart, 0, grantPeriod, 0, 0, 0)); + pool.grant( + Alice, + TimelockTokenPool.Grant(10_000e18, strikePrice1, grantStart, 0, grantPeriod, 0, 0, 0) + ); + pool.grant( + Alice, + TimelockTokenPool.Grant(20_000e18, strikePrice2, grantStart, 0, grantPeriod, 0, 0, 0) + ); vm.prank(Vault); tko.approve(address(pool), 30_000e18); + uint256 halfTimeWithdrawCost = + (5000e18 / ONE_TKO_UNIT * strikePrice1) + (10_000e18 / ONE_TKO_UNIT * strikePrice2); + vm.warp(grantStart + 50 days); ( uint128 amountOwned, uint128 amountUnlocked, uint128 amountWithdrawn, - uint128 amountWithdrawable + uint128 amountToWithdraw, + uint128 costToWithdraw ) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 15_000e18); assertEq(amountUnlocked, 15_000e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 15_000e18); + assertEq(amountToWithdraw, 15_000e18); + assertEq(costToWithdraw, halfTimeWithdrawCost); pool.void(Alice); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 15_000e18); assertEq(amountUnlocked, 15_000e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 15_000e18); + assertEq(amountToWithdraw, 15_000e18); + assertEq(costToWithdraw, halfTimeWithdrawCost); vm.warp(grantStart + 100 days); - (amountOwned, amountUnlocked, amountWithdrawn, amountWithdrawable) = + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); assertEq(amountOwned, 15_000e18); assertEq(amountUnlocked, 15_000e18); assertEq(amountWithdrawn, 0); - assertEq(amountWithdrawable, 15_000e18); + assertEq(amountToWithdraw, 15_000e18); + assertEq(costToWithdraw, halfTimeWithdrawCost); + } + + function test_correct_strike_price() public { + uint64 grantStart = uint64(block.timestamp); + uint32 grantPeriod = 4 * 365 days; + uint64 grantCliff = grantStart + 90 days; + + uint64 unlockStart = grantStart + 365 days; + uint32 unlockPeriod = 4 * 365 days; + uint64 unlockCliff = unlockStart + 365 days; + + uint64 strikePrice = 10_000; // 0.01 USDC if decimals are 6 (as in our test) + + pool.grant( + Alice, + TimelockTokenPool.Grant( + 10_000e18, + strikePrice, + grantStart, + grantCliff, + grantPeriod, + unlockStart, + unlockCliff, + unlockPeriod + ) + ); + vm.prank(Vault); + tko.approve(address(pool), 10_000e18); + + ( + uint128 amountOwned, + uint128 amountUnlocked, + uint128 amountWithdrawn, + uint128 amountToWithdraw, + uint128 costToWithdraw + ) = pool.getMyGrantSummary(Alice); + assertEq(amountOwned, 0); + assertEq(amountUnlocked, 0); + assertEq(amountWithdrawn, 0); + assertEq(amountToWithdraw, 0); + assertEq(costToWithdraw, 0); + + // When withdraw (5 years later) - check if correct price is deducted + vm.warp(grantStart + 5 * 365 days); + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = + pool.getMyGrantSummary(Alice); + assertEq(amountOwned, 10_000e18); + assertEq(amountUnlocked, 10_000e18); + assertEq(amountWithdrawn, 0); + assertEq(amountToWithdraw, 10_000e18); + + // 10_000 TKO tokens * strikePrice + uint256 payedUsdc = 10_000 * strikePrice; + + vm.prank(Alice); + usdc.approve(address(pool), payedUsdc); + + vm.prank(Alice); + pool.withdraw(); + assertEq(tko.balanceOf(Alice), 10_000e18); + assertEq(usdc.balanceOf(Alice), 1_000_000_000e6 - payedUsdc); + } + + function test_correct_strike_price_if_multiple_grants_different_price() public { + uint64 grantStart = uint64(block.timestamp); + uint32 grantPeriod = 4 * 365 days; + uint64 grantCliff = grantStart + 90 days; + + uint64 unlockStart = grantStart + 365 days; + uint32 unlockPeriod = 4 * 365 days; + uint64 unlockCliff = unlockStart + 365 days; + + // Grant Alice 2 times (2x 10_000), with different strik price + pool.grant( + Alice, + TimelockTokenPool.Grant( + 10_000e18, + strikePrice1, + grantStart, + grantCliff, + grantPeriod, + unlockStart, + unlockCliff, + unlockPeriod + ) + ); + + pool.grant( + Alice, + TimelockTokenPool.Grant( + 10_000e18, + strikePrice2, + grantStart, + grantCliff, + grantPeriod, + unlockStart, + unlockCliff, + unlockPeriod + ) + ); + vm.prank(Vault); + tko.approve(address(pool), 20_000e18); + + ( + uint128 amountOwned, + uint128 amountUnlocked, + uint128 amountWithdrawn, + uint128 amountToWithdraw, + uint128 costToWithdraw + ) = pool.getMyGrantSummary(Alice); + assertEq(amountOwned, 0); + assertEq(amountUnlocked, 0); + assertEq(amountWithdrawn, 0); + assertEq(amountToWithdraw, 0); + + // When withdraw (5 years later) - check if correct price is deducted + vm.warp(grantStart + 5 * 365 days); + (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = + pool.getMyGrantSummary(Alice); + assertEq(amountOwned, 20_000e18); + assertEq(amountUnlocked, 20_000e18); + assertEq(amountWithdrawn, 0); + assertEq(amountToWithdraw, 20_000e18); + + // 10_000 TKO * strikePrice1 + 10_000 TKO * strikePrice2 + uint256 payedUsdc = 10_000 * strikePrice1 + 10_000 * strikePrice2; + + vm.prank(Alice); + usdc.approve(address(pool), payedUsdc); + + vm.prank(Alice); + pool.withdraw(); + assertEq(tko.balanceOf(Alice), 20_000e18); + assertEq(usdc.balanceOf(Alice), 1_000_000_000e6 - payedUsdc); } } From 6bc121cc5c857a73026f54034e71ad854c54802f Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Thu, 18 Jan 2024 22:02:39 +0800 Subject: [PATCH 2/5] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 353963f792d..49c0a65bda0 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -133,8 +133,8 @@ contract TimelockTokenPool is EssentialContract { function void(address recipient) external onlyOwner { Recipient storage r = recipients[recipient]; uint128 amountVoided; - uint256 len = r.grants.length; - for (uint128 i; i < len; ++i) { + uint256 rGrantsLength = r.grants.length; + for (uint128 i; i < rGrantsLength; ++i) { amountVoided += _voidGrant(r.grants[i]); } if (amountVoided == 0) revert NOTHING_TO_VOID(); @@ -168,9 +168,9 @@ contract TimelockTokenPool is EssentialContract { ) { Recipient storage r = recipients[recipient]; - uint256 len = r.grants.length; + uint256 rGrantsLength = r.grants.length; uint128 totalCost; - for (uint128 i; i < len; ++i) { + for (uint128 i; i < rGrantsLength; ++i) { amountOwned += _getAmountOwned(r.grants[i]); uint128 _amountUnlocked = _getAmountUnlocked(r.grants[i]); From 73a872ac6a0be4306c0d793630a386f6948078c8 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Thu, 18 Jan 2024 22:05:16 +0800 Subject: [PATCH 3/5] Update TimelockTokenPool.sol --- packages/protocol/contracts/team/TimelockTokenPool.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/protocol/contracts/team/TimelockTokenPool.sol b/packages/protocol/contracts/team/TimelockTokenPool.sol index 49c0a65bda0..663ddf40a17 100644 --- a/packages/protocol/contracts/team/TimelockTokenPool.sol +++ b/packages/protocol/contracts/team/TimelockTokenPool.sol @@ -196,6 +196,9 @@ contract TimelockTokenPool is EssentialContract { r.amountWithdrawn += amountToWithdraw; r.costPaid += costToWithdraw; + totalAmountWithdrawn += amountToWithdraw; + totalCostPaid += costToWithdraw; + IERC20(taikoToken).transferFrom(sharedVault, to, amountToWithdraw); IERC20(costToken).transferFrom(recipient, sharedVault, costToWithdraw); From 1cb25fc1befd393599f803de5b6f275ecb68c0e2 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Thu, 18 Jan 2024 23:00:57 +0800 Subject: [PATCH 4/5] Update TimelockTokenPool.t.sol --- packages/protocol/test/team/TimelockTokenPool.t.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/protocol/test/team/TimelockTokenPool.t.sol b/packages/protocol/test/team/TimelockTokenPool.t.sol index 7bf50051018..b91147802de 100644 --- a/packages/protocol/test/team/TimelockTokenPool.t.sol +++ b/packages/protocol/test/team/TimelockTokenPool.t.sol @@ -131,9 +131,7 @@ contract TestTimelockTokenPool is TaikoTest { (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); - // TODO(dani): who did you figured out this number? It's really hard to maintain - uint256 amount1 = 5_000_000_317_097_919_837_645; - + uint256 amount1 = uint128(10_000e18) * uint64(block.timestamp - unlockStart) / unlockPeriod; uint256 expectedCost = amount1 / ONE_TKO_UNIT * strikePrice1; assertEq(amountOwned, 10_000e18); @@ -217,7 +215,7 @@ contract TestTimelockTokenPool is TaikoTest { (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); - uint256 amount1 = 5_000_000_317_097_919_837_645; + uint256 amount1 = uint128(10_000e18) * uint64(block.timestamp - grantStart) / grantPeriod; uint256 expectedCost = amount1 / ONE_TKO_UNIT * strikePrice1; assertEq(amountOwned, amount1); From 0c7af558d9773c54c4aa503c3ef51bbffd1445a8 Mon Sep 17 00:00:00 2001 From: Daniel Wang Date: Thu, 18 Jan 2024 23:03:51 +0800 Subject: [PATCH 5/5] Update TimelockTokenPool.t.sol --- packages/protocol/test/team/TimelockTokenPool.t.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/protocol/test/team/TimelockTokenPool.t.sol b/packages/protocol/test/team/TimelockTokenPool.t.sol index b91147802de..08fbc42ad69 100644 --- a/packages/protocol/test/team/TimelockTokenPool.t.sol +++ b/packages/protocol/test/team/TimelockTokenPool.t.sol @@ -148,15 +148,13 @@ contract TestTimelockTokenPool is TaikoTest { (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); - expectedCost = (10_000e18 - amount1) / ONE_TKO_UNIT * strikePrice1; + expectedCost = amount1 / ONE_TKO_UNIT * strikePrice1; assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, amount1); assertEq(amountToWithdraw, 10_000e18 - amount1); - - // TODO(dani): the following assert fails - // assertEq(costToWithdraw , expectedCost); + assertEq(costToWithdraw, expectedCost); vm.prank(Alice); pool.withdraw(); @@ -232,13 +230,12 @@ contract TestTimelockTokenPool is TaikoTest { (amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) = pool.getMyGrantSummary(Alice); - expectedCost = (10_000e18 - amount1) / ONE_TKO_UNIT * strikePrice1; + expectedCost = amount1 / ONE_TKO_UNIT * strikePrice1; assertEq(amountOwned, 10_000e18); assertEq(amountUnlocked, 10_000e18); assertEq(amountWithdrawn, amount1); assertEq(amountToWithdraw, 10_000e18 - amount1); - // TODO(dani): the following assert fails - // assertEq(costToWithdraw , expectedCost); + assertEq(costToWithdraw, expectedCost); vm.prank(Alice); pool.withdraw();