From 36c1b525b3dac5eac14da603c9c5ce81bdba6847 Mon Sep 17 00:00:00 2001 From: aalavandhann <6264334+aalavandhan@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:04:01 -0500 Subject: [PATCH 1/3] added view method to preview rewards --- contracts/TokenGeyser.sol | 133 +++++++++++++++++------- hardhat.config.ts | 1 + test/token_unlock.ts | 209 ++++++++++++++++++++++++++++++-------- test/unstake.ts | 2 +- 4 files changed, 264 insertions(+), 81 deletions(-) diff --git a/contracts/TokenGeyser.sol b/contracts/TokenGeyser.sol index b47df57..534fc12 100644 --- a/contracts/TokenGeyser.sol +++ b/contracts/TokenGeyser.sol @@ -195,9 +195,7 @@ contract TokenGeyser is "TokenGeyser: Staking shares exist, but no staking tokens do" ); - uint256 mintedStakingShares = (totalStakingShares > 0) - ? totalStakingShares.mul(amount).div(totalStaked()) - : amount.mul(initialSharesPerToken); + uint256 mintedStakingShares = computeStakingShares(amount); require(mintedStakingShares > 0, "TokenGeyser: Stake amount is too small"); _updateAccounting(); @@ -369,20 +367,9 @@ contract TokenGeyser is /** * @notice A globally callable function to update the accounting state of the system. * Global state and state for the caller are updated. - * @return [0] balance of the locked pool - * @return [1] balance of the unlocked pool - * @return [2] caller's staking share seconds - * @return [3] global staking share seconds - * @return [4] Rewards caller has accumulated, optimistically assumes max time-bonus. - * @return [5] block timestamp */ - function updateAccounting() - external - nonReentrant - whenNotPaused - returns (uint256, uint256, uint256, uint256, uint256, uint256) - { - return _updateAccounting(); + function updateAccounting() external nonReentrant whenNotPaused { + _updateAccounting(); } /** @@ -407,12 +394,98 @@ contract TokenGeyser is } /** - * @notice Moves distribution tokens from the locked pool to the unlocked pool, according to the - * previously defined unlock schedules. Publicly callable. - * @return Number of newly unlocked distribution tokens. + * @param amount The amounted of tokens staked. + * @return Total number staking shares minted to the user. */ - function unlockTokens() external nonReentrant whenNotPaused returns (uint256) { - return _unlockTokens(); + function computeStakingShares(uint256 amount) public view returns (uint256) { + return + (totalStakingShares > 0) + ? totalStakingShares.mul(amount).div(totalStaked()) + : amount.mul(initialSharesPerToken); + } + + /** + * @notice Computes rewards and pool stats after `durationSec` has elapsed. + * @param durationSec The amount of time in seconds the user continues to participate in the program. + * @param addr The beneficiary wallet address. + * @param additionalStake Any additional stake the user makes at the current block. + * @return [0] Total rewards locked. + * @return [1] Total rewards unlocked. + * @return [2] Amount staked by the user. + * @return [3] Total amount staked by all users. + * @return [4] Total rewards unlocked. + * @return [5] Timestamp after `durationSec`. + */ + function previewRewards( + uint256 durationSec, + address addr, + uint256 additionalStake + ) external view returns (uint256, uint256, uint256, uint256, uint256, uint256) { + uint256 endTimestampSec = block.timestamp.add(durationSec); + + // Compute unlock schedule + uint256 unlockedTokens = 0; + { + uint256 unlockedShares = 0; + for (uint256 s = 0; s < unlockSchedules.length; s++) { + UnlockSchedule memory schedule = unlockSchedules[s]; + uint256 unlockedScheduleShares = (endTimestampSec >= schedule.endAtSec) + ? schedule.initialLockedShares.sub(schedule.unlockedShares) + : endTimestampSec + .sub(schedule.lastUnlockTimestampSec) + .mul(schedule.initialLockedShares) + .div(schedule.durationSec); + unlockedShares = unlockedShares.add(unlockedScheduleShares); + } + unlockedTokens = (totalLockedShares > 0) + ? unlockedShares.mul(totalLocked()).div(totalLockedShares) + : 0; + } + uint256 totalLocked_ = totalLocked().sub(unlockedTokens); + uint256 totalUnlocked_ = totalUnlocked().add(unlockedTokens); + + // Compute new accounting state + uint256 userStake = totalStakedBy(addr).add(additionalStake); + uint256 totalStaked_ = totalStaked().add(additionalStake); + + // Compute user's stake and rewards + uint256 userRewards = 0; + { + uint256 additionalStakingShareSeconds = durationSec.mul( + computeStakingShares(additionalStake) + ); + uint256 newStakingShareSeconds = block + .timestamp + .sub(lastAccountingTimestampSec) + .add(durationSec) + .mul(totalStakingShares); + uint256 totalStakingShareSeconds_ = totalStakingShareSeconds + .add(newStakingShareSeconds) + .add(additionalStakingShareSeconds); + uint256 newUserStakingShareSeconds = block + .timestamp + .sub(userTotals[addr].lastAccountingTimestampSec) + .add(durationSec) + .mul(userTotals[addr].stakingShares); + uint256 userStakingShareSeconds = userTotals[addr] + .stakingShareSeconds + .add(newUserStakingShareSeconds) + .add(additionalStakingShareSeconds); + userRewards = (totalStakingShareSeconds_ > 0) + ? totalUnlocked_.mul(userStakingShareSeconds).div( + totalStakingShareSeconds_ + ) + : 0; + } + + return ( + totalLocked_, + totalUnlocked_, + userStake, + totalStaked_, + userRewards, + endTimestampSec + ); } //------------------------------------------------------------------------- @@ -482,10 +555,7 @@ contract TokenGeyser is /** * @dev Updates time-dependent global storage state. */ - function _updateAccounting() - private - returns (uint256, uint256, uint256, uint256, uint256, uint256) - { + function _updateAccounting() private { _unlockTokens(); // Global accounting @@ -506,19 +576,6 @@ contract TokenGeyser is newUserStakingShareSeconds ); user.lastAccountingTimestampSec = block.timestamp; - - uint256 totalUserRewards = (totalStakingShareSeconds > 0) - ? totalUnlocked().mul(user.stakingShareSeconds).div(totalStakingShareSeconds) - : 0; - - return ( - totalLocked(), - totalUnlocked(), - user.stakingShareSeconds, - totalStakingShareSeconds, - totalUserRewards, - block.timestamp - ); } /** diff --git a/hardhat.config.ts b/hardhat.config.ts index 4d8e422..b968bf8 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -66,6 +66,7 @@ export default { enabled: true, runs: 750, }, + viaIR: true, }, }, ], diff --git a/test/token_unlock.ts b/test/token_unlock.ts index b717931..a287d2b 100644 --- a/test/token_unlock.ts +++ b/test/token_unlock.ts @@ -47,8 +47,14 @@ async function setupContracts() { async function checkAvailableToUnlock(dist, v) { const u = await dist.totalUnlocked.staticCall(); - const r = await dist.updateAccounting.staticCall(); - // console.log('Total unlocked: ', u.toString(), 'total unlocked after: ', r[1].toString()); + const from = await owner.getAddress(); + const r = await dist.previewRewards.staticCall(0, from, 0); + // console.log( + // "Total unlocked: ", + // u.toString(), + // "total unlocked after: ", + // r[1].toString(), + // ); checkAmplAprox(r[1] - u, v); } @@ -465,63 +471,182 @@ describe("LockedPool", function () { }); }); - describe("updateAccounting", function () { + describe("previewRewards", function () { let _r, _t; beforeEach(async function () { - _r = await dist.updateAccounting.staticCall({ from: owner }); + await ampl.transfer(anotherAccount.getAddress(), $AMPL(1000)); + _r = await dist.previewRewards(0, await owner.getAddress(), 0); _t = await TimeHelpers.currentTime(); await ampl.approve(dist.target, $AMPL(300)); await dist.stake($AMPL(100)); await dist.lockTokens($AMPL(100), ONE_YEAR); await TimeHelpers.increaseTime(ONE_YEAR / 2); await dist.lockTokens($AMPL(100), ONE_YEAR); + await ampl.connect(anotherAccount).approve(dist.target, $AMPL(200)); + await dist.connect(anotherAccount).stake($AMPL(200)); await TimeHelpers.increaseTime(ONE_YEAR / 10); }); describe("when user history does exist", async function () { - it("should return the system state", async function () { - const r = await dist.updateAccounting.staticCall(); - const t = await TimeHelpers.currentTime(); - checkAmplAprox(r[0], 130); - checkAmplAprox(r[1], 70); - const timeElapsed = t - _t; - expect(r[2] / $AMPL(100) / InitialSharesPerToken) - .to.gte(timeElapsed - 5) - .to.lte(timeElapsed + 5); - expect(r[3] / $AMPL(100) / InitialSharesPerToken) - .to.gte(timeElapsed - 5) - .to.lte(timeElapsed + 5); - checkAmplAprox(r[4], 70); - checkAmplAprox(r[4], 70); - expect(r[5] - _r[5]) - .to.gte(timeElapsed - 1) - .to.lte(timeElapsed + 1); + describe("current state, without additional stake", function () { + it("should return the system state", async function () { + const r = await dist.previewRewards(0, await owner.getAddress(), 0); + const t = await TimeHelpers.currentTime(); + checkAmplAprox(r[0], 130); + checkAmplAprox(r[1], 70); + expect(r[2]).to.eq($AMPL(100)); + expect(r[3]).to.eq($AMPL(300)); + checkAmplAprox(r[4], 52.5); + const timeElapsed = t - _t; + expect(r[5] - _r[5]) + .to.gte(timeElapsed - 1) + .to.lte(timeElapsed + 1); + await expect(await dist.unstake($AMPL(100))) + .to.emit(dist, "TokensClaimed") + .withArgs(await owner.getAddress(), "52500015952"); + }); + }); + + describe("current state, with additional stake", function () { + it("should return the system state", async function () { + const r = await dist.previewRewards(0, await owner.getAddress(), $AMPL(100)); + const t = await TimeHelpers.currentTime(); + checkAmplAprox(r[0], 130); + checkAmplAprox(r[1], 70); + expect(r[2]).to.eq($AMPL(200)); + expect(r[3]).to.eq($AMPL(400)); + checkAmplAprox(r[4], 52.5); + const timeElapsed = t - _t; + expect(r[5] - _r[5]) + .to.gte(timeElapsed - 1) + .to.lte(timeElapsed + 1); + await ampl.approve(dist.target, $AMPL(100)); + await dist.stake($AMPL(100)); + await expect(await dist.unstake($AMPL(200))) + .to.emit(dist, "TokensClaimed") + .withArgs(await owner.getAddress(), "52500017834"); + }); + }); + + describe("after 3 months, without additional stake", function () { + it("should return the system state", async function () { + const r = await dist.previewRewards(ONE_YEAR / 4, await owner.getAddress(), 0); + const t = await TimeHelpers.currentTime(); + checkAmplAprox(r[0], 80); + checkAmplAprox(r[1], 120); + expect(r[2]).to.eq($AMPL(100)); + expect(r[3]).to.eq($AMPL(300)); + checkAmplAprox(r[4], 65.8); + const timeElapsed = t - _t + ONE_YEAR / 4; + expect(r[5] - _r[5]) + .to.gte(timeElapsed - 1) + .to.lte(timeElapsed + 1); + + await TimeHelpers.increaseTime(ONE_YEAR / 4); + await expect(await dist.unstake($AMPL(100))) + .to.emit(dist, "TokensClaimed") + .withArgs(await owner.getAddress(), "65806466635"); + }); + }); + + describe("after 3 months, with additional stake", function () { + it("should return the system state", async function () { + const r = await dist.previewRewards( + ONE_YEAR / 4, + await owner.getAddress(), + $AMPL(100), + ); + const t = await TimeHelpers.currentTime(); + checkAmplAprox(r[0], 80); + checkAmplAprox(r[1], 120); + expect(r[2]).to.eq($AMPL(200)); + expect(r[3]).to.eq($AMPL(400)); + checkAmplAprox(r[4], 73.3333); + const timeElapsed = t - _t + ONE_YEAR / 4; + expect(r[5] - _r[5]) + .to.gte(timeElapsed - 1) + .to.lte(timeElapsed + 1); + await ampl.approve(dist.target, $AMPL(100)); + await dist.stake($AMPL(100)); + await TimeHelpers.increaseTime(ONE_YEAR / 4); + await expect(await dist.unstake($AMPL(200))) + .to.emit(dist, "TokensClaimed") + .withArgs(await owner.getAddress(), "73333353473"); + await TimeHelpers.increaseTime(ONE_YEAR * 10); + await expect(await dist.connect(anotherAccount).unstake($AMPL(200))) + .to.emit(dist, "TokensClaimed") + .withArgs(await anotherAccount.getAddress(), "126666646527"); + }); }); }); describe("when user history does not exist", async function () { - it("should return the system state", async function () { - const r = dist.interface.decodeFunctionResult( - "updateAccounting", - await ethers.provider.call({ - from: ethers.ZeroAddress, - to: dist.target, - data: dist.interface.encodeFunctionData("updateAccounting"), - }), - ); + describe("current state, with no additional stake", function () { + it("should return the system state", async function () { + const r = await dist.previewRewards(0, ethers.ZeroAddress, 0); + const t = await TimeHelpers.currentTime(); + checkAmplAprox(r[0], 130); + checkAmplAprox(r[1], 70); + expect(r[2]).to.eq(0n); + expect(r[3]).to.eq($AMPL(300)); + checkAmplAprox(r[4], 0); + const timeElapsed = t - _t; + expect(r[5] - _r[5]) + .to.gte(timeElapsed - 1) + .to.lte(timeElapsed + 1); + }); + }); - const t = await TimeHelpers.currentTime(); - checkAmplAprox(r[0], 130); - checkAmplAprox(r[1], 70); - const timeElapsed = t - _t; - expect(r[2] / $AMPL(100) / InitialSharesPerToken).to.eq(0n); - expect(r[3] / $AMPL(100) / InitialSharesPerToken) - .to.gte(timeElapsed - 5) - .to.lte(timeElapsed + 5); - checkAmplAprox(r[4], 0); - expect(r[5] - _r[5]) - .to.gte(timeElapsed - 1) - .to.lte(timeElapsed + 1); + describe("current state, with additional stake", function () { + it("should return the system state", async function () { + const r = await dist.previewRewards(0, ethers.ZeroAddress, $AMPL(100)); + const t = await TimeHelpers.currentTime(); + checkAmplAprox(r[0], 130); + checkAmplAprox(r[1], 70); + expect(r[2]).to.eq($AMPL(100)); + expect(r[3]).to.eq($AMPL(400)); + checkAmplAprox(r[4], 0); + const timeElapsed = t - _t; + expect(r[5] - _r[5]) + .to.gte(timeElapsed - 1) + .to.lte(timeElapsed + 1); + }); + }); + + describe("after 3 months, without additional stake", function () { + it("should return the system state", async function () { + const r = await dist.previewRewards(ONE_YEAR / 4, ethers.ZeroAddress, 0); + const t = await TimeHelpers.currentTime(); + checkAmplAprox(r[0], 79.99); + checkAmplAprox(r[1], 120); + expect(r[2]).to.eq(0n); + expect(r[3]).to.eq($AMPL(300)); + checkAmplAprox(r[4], 0); + const timeElapsed = t - _t + ONE_YEAR / 4; + expect(r[5] - _r[5]) + .to.gte(timeElapsed - 1) + .to.lte(timeElapsed + 1); + }); + }); + + describe("after 3 months, with additional stake", function () { + it("should return the system state", async function () { + const r = await dist.previewRewards( + ONE_YEAR / 4, + ethers.ZeroAddress, + $AMPL(100), + ); + const t = await TimeHelpers.currentTime(); + checkAmplAprox(r[0], 79.99); + checkAmplAprox(r[1], 120); + expect(r[2]).to.eq($AMPL(100)); + expect(r[3]).to.eq($AMPL(400)); + checkAmplAprox(r[4], 16.666); + const timeElapsed = t - _t + ONE_YEAR / 4; + expect(r[5] - _r[5]) + .to.gte(timeElapsed - 1) + .to.lte(timeElapsed + 1); + }); }); }); }); diff --git a/test/unstake.ts b/test/unstake.ts index cf389a7..b3a7b13 100644 --- a/test/unstake.ts +++ b/test/unstake.ts @@ -45,7 +45,7 @@ async function setupContracts() { } async function totalRewardsFor(account) { - const r = await dist.connect(account).updateAccounting.staticCall(); + const r = await dist.previewRewards.staticCall(0, await account.getAddress(), 0); return r[4]; } From c49aa14dae842a0a71b0c99e34c4790352378dc5 Mon Sep 17 00:00:00 2001 From: aalavandhann <6264334+aalavandhan@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:59:34 -0500 Subject: [PATCH 2/3] adjusting for bonus rewards in the preview function --- contracts/TokenGeyser.sol | 68 ++++++++++++++++++++++++--------------- test/helper.ts | 5 +-- test/token_unlock.ts | 10 ++++-- test/unstake.ts | 34 ++++++++++---------- 4 files changed, 70 insertions(+), 47 deletions(-) diff --git a/contracts/TokenGeyser.sol b/contracts/TokenGeyser.sol index 534fc12..6666dab 100644 --- a/contracts/TokenGeyser.sol +++ b/contracts/TokenGeyser.sol @@ -60,6 +60,7 @@ contract TokenGeyser is // Time-bonus params // uint256 public constant BONUS_DECIMALS = 2; + uint256 public constant BONUS_HUNDRED_PERC = 10 ** BONUS_DECIMALS; uint256 public startBonus; uint256 public bonusPeriodSec; @@ -248,6 +249,7 @@ contract TokenGeyser is uint256 stakingShareSecondsToBurn = 0; uint256 sharesLeftToBurn = stakingSharesToBurn; uint256 rewardAmount = 0; + uint256 totalUnlocked_ = totalUnlocked(); while (sharesLeftToBurn > 0) { Stake storage lastStake = accountStakes[accountStakes.length - 1]; uint256 stakeTimeSec = block.timestamp.sub(lastStake.timestampSec); @@ -258,7 +260,9 @@ contract TokenGeyser is rewardAmount = computeNewReward( rewardAmount, newStakingShareSecondsToBurn, - stakeTimeSec + totalStakingShareSeconds, + stakeTimeSec, + totalUnlocked_ ); stakingShareSecondsToBurn = stakingShareSecondsToBurn.add( newStakingShareSecondsToBurn @@ -271,7 +275,9 @@ contract TokenGeyser is rewardAmount = computeNewReward( rewardAmount, newStakingShareSecondsToBurn, - stakeTimeSec + totalStakingShareSeconds, + stakeTimeSec, + totalUnlocked_ ); stakingShareSecondsToBurn = stakingShareSecondsToBurn.add( newStakingShareSecondsToBurn @@ -318,29 +324,32 @@ contract TokenGeyser is * unstake op. Any bonuses are already applied. * @param stakingShareSeconds The stakingShare-seconds that are being burned for new * distribution tokens. + * @param totalStakingShareSeconds_ The total stakingShare-seconds. * @param stakeTimeSec Length of time for which the tokens were staked. Needed to calculate * the time-bonus. + * @param totalUnlocked_ The reward tokens currently unlocked. * @return Updated amount of distribution tokens to award, with any bonus included on the * newly added tokens. */ function computeNewReward( uint256 currentRewardTokens, uint256 stakingShareSeconds, - uint256 stakeTimeSec + uint256 totalStakingShareSeconds_, + uint256 stakeTimeSec, + uint256 totalUnlocked_ ) public view returns (uint256) { - uint256 newRewardTokens = totalUnlocked().mul(stakingShareSeconds).div( - totalStakingShareSeconds - ); + uint256 newRewardTokens = (totalStakingShareSeconds_ > 0) + ? totalUnlocked_.mul(stakingShareSeconds).div(totalStakingShareSeconds_) + : 0; if (stakeTimeSec >= bonusPeriodSec) { return currentRewardTokens.add(newRewardTokens); } - uint256 oneHundredPct = 10 ** BONUS_DECIMALS; uint256 bonusedReward = startBonus - .add(oneHundredPct.sub(startBonus).mul(stakeTimeSec).div(bonusPeriodSec)) + .add(BONUS_HUNDRED_PERC.sub(startBonus).mul(stakeTimeSec).div(bonusPeriodSec)) .mul(newRewardTokens) - .div(oneHundredPct); + .div(BONUS_HUNDRED_PERC); return currentRewardTokens.add(bonusedReward); } @@ -448,12 +457,13 @@ contract TokenGeyser is uint256 userStake = totalStakedBy(addr).add(additionalStake); uint256 totalStaked_ = totalStaked().add(additionalStake); - // Compute user's stake and rewards - uint256 userRewards = 0; + // Compute user's final stake share and rewards + uint256 rewardAmount = 0; { uint256 additionalStakingShareSeconds = durationSec.mul( computeStakingShares(additionalStake) ); + uint256 newStakingShareSeconds = block .timestamp .sub(lastAccountingTimestampSec) @@ -462,20 +472,26 @@ contract TokenGeyser is uint256 totalStakingShareSeconds_ = totalStakingShareSeconds .add(newStakingShareSeconds) .add(additionalStakingShareSeconds); - uint256 newUserStakingShareSeconds = block - .timestamp - .sub(userTotals[addr].lastAccountingTimestampSec) - .add(durationSec) - .mul(userTotals[addr].stakingShares); - uint256 userStakingShareSeconds = userTotals[addr] - .stakingShareSeconds - .add(newUserStakingShareSeconds) - .add(additionalStakingShareSeconds); - userRewards = (totalStakingShareSeconds_ > 0) - ? totalUnlocked_.mul(userStakingShareSeconds).div( - totalStakingShareSeconds_ - ) - : 0; + + Stake[] memory accountStakes = userStakes[addr]; + for (uint256 s = 0; s < accountStakes.length; s++) { + Stake memory stake_ = accountStakes[s]; + uint256 stakeDurationSec = endTimestampSec.sub(stake_.timestampSec); + rewardAmount = computeNewReward( + rewardAmount, + stake_.stakingShares.mul(stakeDurationSec), + totalStakingShareSeconds_, + durationSec, + totalUnlocked_ + ); + } + rewardAmount = computeNewReward( + rewardAmount, + additionalStakingShareSeconds, + totalStakingShareSeconds_, + durationSec, + totalUnlocked_ + ); } return ( @@ -483,7 +499,7 @@ contract TokenGeyser is totalUnlocked_, userStake, totalStaked_, - userRewards, + rewardAmount, endTimestampSec ); } diff --git a/test/helper.ts b/test/helper.ts index ceb5c54..aca6fed 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -22,8 +22,8 @@ function checkSharesAprox(x, y) { function checkAprox(x, y, delta_) { const delta = BigInt(delta_); - const upper = y + delta; - const lower = y - delta; + const upper = BigInt(y) + delta; + const lower = BigInt(y) - delta; expect(x).to.gte(lower).to.lte(upper); } @@ -89,6 +89,7 @@ async function deployGeyser(owner, params) { } module.exports = { + checkAprox, checkAmplAprox, checkSharesAprox, invokeRebase, diff --git a/test/token_unlock.ts b/test/token_unlock.ts index a287d2b..84ba6a7 100644 --- a/test/token_unlock.ts +++ b/test/token_unlock.ts @@ -5,6 +5,7 @@ import { TimeHelpers, $AMPL, invokeRebase, + checkAprox, checkAmplAprox, checkSharesAprox, deployGeyser, @@ -480,11 +481,14 @@ describe("LockedPool", function () { await ampl.approve(dist.target, $AMPL(300)); await dist.stake($AMPL(100)); await dist.lockTokens($AMPL(100), ONE_YEAR); + checkAprox(await dist.unlockDuration(), ONE_YEAR, 86400); await TimeHelpers.increaseTime(ONE_YEAR / 2); await dist.lockTokens($AMPL(100), ONE_YEAR); + checkAprox(await dist.unlockDuration(), ONE_YEAR, 86400); await ampl.connect(anotherAccount).approve(dist.target, $AMPL(200)); await dist.connect(anotherAccount).stake($AMPL(200)); await TimeHelpers.increaseTime(ONE_YEAR / 10); + checkAprox(await dist.unlockDuration(), (ONE_YEAR * 9) / 10, 86400); }); describe("when user history does exist", async function () { @@ -496,7 +500,7 @@ describe("LockedPool", function () { checkAmplAprox(r[1], 70); expect(r[2]).to.eq($AMPL(100)); expect(r[3]).to.eq($AMPL(300)); - checkAmplAprox(r[4], 52.5); + checkAmplAprox(r[4], 26.25); const timeElapsed = t - _t; expect(r[5] - _r[5]) .to.gte(timeElapsed - 1) @@ -515,7 +519,7 @@ describe("LockedPool", function () { checkAmplAprox(r[1], 70); expect(r[2]).to.eq($AMPL(200)); expect(r[3]).to.eq($AMPL(400)); - checkAmplAprox(r[4], 52.5); + checkAmplAprox(r[4], 26.25); const timeElapsed = t - _t; expect(r[5] - _r[5]) .to.gte(timeElapsed - 1) @@ -569,6 +573,7 @@ describe("LockedPool", function () { await ampl.approve(dist.target, $AMPL(100)); await dist.stake($AMPL(100)); await TimeHelpers.increaseTime(ONE_YEAR / 4); + checkAprox(await dist.unlockDuration(), 0.65 * ONE_YEAR, 86400); await expect(await dist.unstake($AMPL(200))) .to.emit(dist, "TokensClaimed") .withArgs(await owner.getAddress(), "73333353473"); @@ -576,6 +581,7 @@ describe("LockedPool", function () { await expect(await dist.connect(anotherAccount).unstake($AMPL(200))) .to.emit(dist, "TokensClaimed") .withArgs(await anotherAccount.getAddress(), "126666646527"); + expect(await dist.unlockDuration()).to.eq(0); }); }); }); diff --git a/test/unstake.ts b/test/unstake.ts index b3a7b13..fee43c2 100644 --- a/test/unstake.ts +++ b/test/unstake.ts @@ -120,7 +120,7 @@ describe("unstaking", function () { await dist.connect(anotherAccount).stake($AMPL(50)); await TimeHelpers.increaseTime(ONE_YEAR); await dist.connect(anotherAccount).updateAccounting(); - checkAmplAprox(await totalRewardsFor(anotherAccount), 100); + checkAmplAprox(await totalRewardsFor(anotherAccount), 50); }); it("should update the total staked and rewards", async function () { await dist.connect(anotherAccount).unstake($AMPL(30)); @@ -128,7 +128,7 @@ describe("unstaking", function () { expect( await dist.totalStakedBy.staticCall(await anotherAccount.getAddress()), ).to.eq($AMPL(20)); - checkAmplAprox(await totalRewardsFor(anotherAccount), 40); + checkAmplAprox(await totalRewardsFor(anotherAccount), 20); }); it("should transfer back staked tokens + rewards", async function () { const _b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); @@ -167,7 +167,7 @@ describe("unstaking", function () { await dist.connect(anotherAccount).stake($AMPL(500)); await TimeHelpers.increaseTime(12 * ONE_HOUR); await dist.connect(anotherAccount).updateAccounting(); - checkAmplAprox(await totalRewardsFor(anotherAccount), 1000); + checkAmplAprox(await totalRewardsFor(anotherAccount), 500); }); it("should update the total staked and rewards", async function () { await dist.connect(anotherAccount).unstake($AMPL(250)); @@ -175,7 +175,7 @@ describe("unstaking", function () { expect( await dist.totalStakedBy.staticCall(await anotherAccount.getAddress()), ).to.eq($AMPL(250)); - checkAmplAprox(await totalRewardsFor(anotherAccount), 625); // (.5 * .75 * 1000) + 250 + checkAmplAprox(await totalRewardsFor(anotherAccount), 312.5); // (.5 * .75 * 1000) + 250 }); it("should transfer back staked tokens + rewards", async function () { const _b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); @@ -217,7 +217,7 @@ describe("unstaking", function () { await dist.connect(anotherAccount).updateAccounting(); }); it("checkTotalRewards", async function () { - checkAmplAprox(await totalRewardsFor(anotherAccount), 51); + checkAmplAprox(await totalRewardsFor(anotherAccount), 25.5); }); it("should update the total staked and rewards", async function () { await dist.connect(anotherAccount).unstake($AMPL(30)); @@ -225,7 +225,7 @@ describe("unstaking", function () { expect( await dist.totalStakedBy.staticCall(await anotherAccount.getAddress()), ).to.eq($AMPL(70)); - checkAmplAprox(await totalRewardsFor(anotherAccount), 40.8); + checkAmplAprox(await totalRewardsFor(anotherAccount), 20.4); }); it("should transfer back staked tokens + rewards", async function () { const _b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); @@ -248,7 +248,7 @@ describe("unstaking", function () { await dist.connect(anotherAccount).stake($AMPL(10)); await TimeHelpers.increaseTime(ONE_YEAR); await dist.connect(anotherAccount).updateAccounting(); - checkAmplAprox(await totalRewardsFor(anotherAccount), 100); + checkAmplAprox(await totalRewardsFor(anotherAccount), 50); }); it("should use updated user accounting", async function () { @@ -291,13 +291,13 @@ describe("unstaking", function () { await dist.connect(anotherAccount).updateAccounting(); await dist.updateAccounting(); expect(await dist.totalStaked.staticCall()).to.eq($AMPL(100)); - checkAmplAprox(await totalRewardsFor(anotherAccount), 45.6); - checkAmplAprox(await totalRewardsFor(owner), 30.4); + checkAmplAprox(await totalRewardsFor(anotherAccount), 22.8); + checkAmplAprox(await totalRewardsFor(owner), 15.2); }); it("checkTotalRewards", async function () { expect(await dist.totalStaked.staticCall()).to.eq($AMPL(100)); - checkAmplAprox(await totalRewardsFor(anotherAccount), 45.6); - checkAmplAprox(await totalRewardsFor(owner), 30.4); + checkAmplAprox(await totalRewardsFor(anotherAccount), 22.8); + checkAmplAprox(await totalRewardsFor(owner), 15.2); }); it("should update the total staked and rewards", async function () { await dist.connect(anotherAccount).unstake($AMPL(30)); @@ -308,8 +308,8 @@ describe("unstaking", function () { expect(await dist.totalStakedBy.staticCall(await owner.getAddress())).to.eq( $AMPL(50), ); - checkAmplAprox(await totalRewardsFor(anotherAccount), 18.24); - checkAmplAprox(await totalRewardsFor(owner), 30.4); + checkAmplAprox(await totalRewardsFor(anotherAccount), 9.12); + checkAmplAprox(await totalRewardsFor(owner), 15.2); }); it("should transfer back staked tokens + rewards", async function () { const _b = await ampl.balanceOf.staticCall(await anotherAccount.getAddress()); @@ -342,8 +342,8 @@ describe("unstaking", function () { await dist.connect(anotherAccount).updateAccounting(); await dist.updateAccounting(); expect(await dist.totalStaked.staticCall()).to.eq($AMPL(18000)); - checkAmplAprox(await totalRewardsFor(anotherAccount), rewardsAnotherAccount); - checkAmplAprox(await totalRewardsFor(owner), rewardsOwner); + checkAmplAprox(await totalRewardsFor(anotherAccount), rewardsAnotherAccount / 2); + checkAmplAprox(await totalRewardsFor(owner), rewardsOwner / 2); }); it("should update the total staked and rewards", async function () { await dist.connect(anotherAccount).unstake($AMPL(10000)); @@ -353,7 +353,7 @@ describe("unstaking", function () { $AMPL(8000), ); checkAmplAprox(await totalRewardsFor(anotherAccount), 0); - checkAmplAprox(await totalRewardsFor(owner), rewardsOwner); + checkAmplAprox(await totalRewardsFor(owner), rewardsOwner / 2); await dist.unstake($AMPL(8000)); expect(await dist.totalStaked.staticCall()).to.eq($AMPL(0)); expect( @@ -389,7 +389,7 @@ describe("unstaking", function () { await dist.connect(anotherAccount).updateAccounting(); }); it("should return the reward amount", async function () { - checkAmplAprox(await totalRewardsFor(anotherAccount), 100); + checkAmplAprox(await totalRewardsFor(anotherAccount), 50); checkAmplAprox( await dist.connect(anotherAccount).unstake.staticCall($AMPL(30)), 60, From 29b6a5ea30ada0b81e05b20d28517ee4d0c85b60 Mon Sep 17 00:00:00 2001 From: aalavandhann <6264334+aalavandhan@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:23:11 -0500 Subject: [PATCH 3/3] helper to return unlock duration --- contracts/TokenGeyser.sol | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/contracts/TokenGeyser.sol b/contracts/TokenGeyser.sol index 6666dab..7e7a8d0 100644 --- a/contracts/TokenGeyser.sol +++ b/contracts/TokenGeyser.sol @@ -6,6 +6,7 @@ import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/P import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { SafeMathCompatibility } from "./_utils/SafeMathCompatibility.sol"; import { ITokenPool } from "./ITokenPool.sol"; @@ -38,6 +39,7 @@ contract TokenGeyser is { using SafeMathCompatibility for uint256; using SafeERC20 for IERC20; + using Math for uint256; //------------------------------------------------------------------------- // Events @@ -413,6 +415,21 @@ contract TokenGeyser is : amount.mul(initialSharesPerToken); } + /** + * @return durationSec The amount of time in seconds when all the reward tokens unlock. + */ + function unlockDuration() external view returns (uint256 durationSec) { + durationSec = 0; + for (uint256 s = 0; s < unlockSchedules.length; s++) { + durationSec = Math.max( + (block.timestamp < unlockSchedules[s].endAtSec) + ? unlockSchedules[s].endAtSec - block.timestamp + : 0, + durationSec + ); + } + } + /** * @notice Computes rewards and pool stats after `durationSec` has elapsed. * @param durationSec The amount of time in seconds the user continues to participate in the program.