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

feat(protocol): One grant per address #15558

Merged
merged 1 commit into from
Jan 24, 2024
Merged
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
36 changes: 11 additions & 25 deletions packages/protocol/contracts/team/TimelockTokenPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,9 @@ contract TimelockTokenPool is EssentialContract {
struct Recipient {
uint128 amountWithdrawn;
uint128 costPaid;
Grant[] grants;
Grant grant;
}

uint256 public constant MAX_GRANTS_PER_ADDRESS = 8;

address public taikoToken;
address public costToken;
address public sharedVault;
Expand All @@ -83,11 +81,11 @@ contract TimelockTokenPool is EssentialContract {
event Voided(address indexed recipient, uint128 amount);
event Withdrawn(address indexed recipient, address to, uint128 amount, uint128 cost);

error ALREADY_GRANTED();
error INVALID_GRANT();
error INVALID_PARAM();
error NOTHING_TO_VOID();
error NOTHING_TO_WITHDRAW();
error TOO_MANY();

function init(
address _taikoToken,
Expand Down Expand Up @@ -116,14 +114,12 @@ contract TimelockTokenPool is EssentialContract {
/// same recipient.
function grant(address recipient, Grant memory g) external onlyOwner {
if (recipient == address(0)) revert INVALID_PARAM();
if (recipients[recipient].grants.length >= MAX_GRANTS_PER_ADDRESS) {
revert TOO_MANY();
}
if (recipients[recipient].grant.amount != 0) revert ALREADY_GRANTED();

_validateGrant(g);

totalAmountGranted += g.amount;
recipients[recipient].grants.push(g);
recipients[recipient].grant = g;
emit Granted(recipient, g);
}

Expand All @@ -132,11 +128,8 @@ contract TimelockTokenPool is EssentialContract {
/// original unlock schedule.
function void(address recipient) external onlyOwner {
Recipient storage r = recipients[recipient];
uint128 amountVoided;
uint256 rGrantsLength = r.grants.length;
for (uint128 i; i < rGrantsLength; ++i) {
amountVoided += _voidGrant(r.grants[i]);
}
uint128 amountVoided = _voidGrant(r.grant);

if (amountVoided == 0) revert NOTHING_TO_VOID();

totalAmountVoided += amountVoided;
Expand Down Expand Up @@ -168,24 +161,17 @@ contract TimelockTokenPool is EssentialContract {
)
{
Recipient storage r = recipients[recipient];
uint256 rGrantsLength = r.grants.length;
uint128 totalCost;
for (uint128 i; i < rGrantsLength; ++i) {
amountOwned += _getAmountOwned(r.grants[i]);

uint128 _amountUnlocked = _getAmountUnlocked(r.grants[i]);
amountUnlocked += _amountUnlocked;

totalCost += _amountUnlocked / 1e18 * r.grants[i].costPerToken;
}
amountOwned = _getAmountOwned(r.grant);
amountUnlocked = _getAmountUnlocked(r.grant);

amountWithdrawn = r.amountWithdrawn;
amountToWithdraw = amountUnlocked - amountWithdrawn;
costToWithdraw = totalCost - r.costPaid;
costToWithdraw = (amountUnlocked / 1e18 * r.grant.costPerToken) - r.costPaid;
}

function getMyGrants(address recipient) public view returns (Grant[] memory) {
return recipients[recipient].grants;
function getMyGrant(address recipient) public view returns (Grant memory) {
return recipients[recipient].grant;
}

function _withdraw(address recipient, address to) private {
Expand Down
163 changes: 29 additions & 134 deletions packages/protocol/test/team/TimelockTokenPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,14 @@ contract TestTimelockTokenPool is TaikoTest {

uint256 amount1 = uint128(10_000e18) * uint64(block.timestamp - grantStart) / grantPeriod;
uint256 expectedCost = amount1 / ONE_TKO_UNIT * strikePrice1;

console2.log("expectedCost", expectedCost);
console2.log("costToWithdraw", costToWithdraw);
assertEq(amountOwned, amount1);
assertEq(amountUnlocked, amount1);
assertEq(amountWithdrawn, 0);
assertEq(amountToWithdraw, amount1);
assertEq(costToWithdraw, expectedCost);
console2.log("EZ feltt elvileg jo volt");

vm.prank(Alice);
pool.withdraw();
Expand Down Expand Up @@ -370,40 +372,15 @@ contract TestTimelockTokenPool is TaikoTest {
assertEq(costToWithdraw, expectedCost);
}

function test_multiple_grants() public {
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 amountToWithdraw,
uint128 costToWithdraw
) = pool.getMyGrantSummary(Alice);
assertEq(amountOwned, 30_000e18);
assertEq(amountUnlocked, 30_000e18);
assertEq(amountWithdrawn, 0);
assertEq(amountToWithdraw, 30_000e18);
assertEq(costToWithdraw, overallCost);
}

function test_void_multiple_grants_before_granted() public {
function test_void_grant_before_granted() public {
uint64 grantStart = uint64(block.timestamp) + 30 days;
pool.grant(Alice, TimelockTokenPool.Grant(10_000e18, 0, grantStart, 0, 0, 0, 0, 0));

vm.expectRevert(TimelockTokenPool.ALREADY_GRANTED.selector);
pool.grant(Alice, TimelockTokenPool.Grant(20_000e18, 0, grantStart, 0, 0, 0, 0, 0));

vm.prank(Vault);
tko.approve(address(pool), 30_000e18);
tko.approve(address(pool), 10_000e18);

(
uint128 amountOwned,
Expand All @@ -421,27 +398,23 @@ contract TestTimelockTokenPool is TaikoTest {
// Try to void the grant
pool.void(Alice);

TimelockTokenPool.Grant[] memory grants = pool.getMyGrants(Alice);
for (uint256 i; i < grants.length; ++i) {
assertEq(grants[i].grantStart, 0);
assertEq(grants[i].grantPeriod, 0);
assertEq(grants[i].grantCliff, 0);

assertEq(grants[i].unlockStart, 0);
assertEq(grants[i].unlockPeriod, 0);
assertEq(grants[i].unlockCliff, 0);
TimelockTokenPool.Grant memory grant = pool.getMyGrant(Alice);

assertEq(grants[i].amount, 0);
}
assertEq(grant.grantStart, 0);
assertEq(grant.grantPeriod, 0);
assertEq(grant.grantCliff, 0);
assertEq(grant.unlockStart, 0);
assertEq(grant.unlockPeriod, 0);
assertEq(grant.unlockCliff, 0);
assertEq(grant.amount, 0);
}

function test_void_multiple_grants_after_granted() public {
function test_void_grant_after_granted() public {
uint64 grantStart = uint64(block.timestamp) + 30 days;
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);
tko.approve(address(pool), 10_000e18);

(
uint128 amountOwned,
Expand All @@ -464,23 +437,18 @@ contract TestTimelockTokenPool is TaikoTest {
pool.void(Alice);
}

function test_void_multiple_grants_in_the_middle() public {
function test_void_grant_in_the_middle() public {
uint64 grantStart = uint64(block.timestamp);
uint32 grantPeriod = 100 days;
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);
tko.approve(address(pool), 10_000e18);

uint256 halfTimeWithdrawCost =
(5000e18 / ONE_TKO_UNIT * strikePrice1) + (10_000e18 / ONE_TKO_UNIT * strikePrice2);
uint256 halfTimeWithdrawCost = 5000e18 / ONE_TKO_UNIT * strikePrice1;

vm.warp(grantStart + 50 days);
(
Expand All @@ -491,29 +459,29 @@ contract TestTimelockTokenPool is TaikoTest {
uint128 costToWithdraw
) = pool.getMyGrantSummary(Alice);

assertEq(amountOwned, 15_000e18);
assertEq(amountUnlocked, 15_000e18);
assertEq(amountOwned, 5000e18);
assertEq(amountUnlocked, 5000e18);
assertEq(amountWithdrawn, 0);
assertEq(amountToWithdraw, 15_000e18);
assertEq(amountToWithdraw, 5000e18);
assertEq(costToWithdraw, halfTimeWithdrawCost);

pool.void(Alice);

(amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) =
pool.getMyGrantSummary(Alice);
assertEq(amountOwned, 15_000e18);
assertEq(amountUnlocked, 15_000e18);
assertEq(amountOwned, 5000e18);
assertEq(amountUnlocked, 5000e18);
assertEq(amountWithdrawn, 0);
assertEq(amountToWithdraw, 15_000e18);
assertEq(amountToWithdraw, 5000e18);
assertEq(costToWithdraw, halfTimeWithdrawCost);

vm.warp(grantStart + 100 days);
(amountOwned, amountUnlocked, amountWithdrawn, amountToWithdraw, costToWithdraw) =
pool.getMyGrantSummary(Alice);
assertEq(amountOwned, 15_000e18);
assertEq(amountUnlocked, 15_000e18);
assertEq(amountOwned, 5000e18);
assertEq(amountUnlocked, 5000e18);
assertEq(amountWithdrawn, 0);
assertEq(amountToWithdraw, 15_000e18);
assertEq(amountToWithdraw, 5000e18);
assertEq(costToWithdraw, halfTimeWithdrawCost);
}

Expand Down Expand Up @@ -577,77 +545,4 @@ contract TestTimelockTokenPool is TaikoTest {
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);
}
}
Loading