From 60d7716b4d94b7f7c52339943dde9f1976709aad 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] adjusting for bonus rewards in the preview function --- contracts/TokenGeyser.sol | 75 +++++++++++++++++++++++---------------- test/helper.ts | 5 +-- test/token_unlock.ts | 50 ++++++++++++++++---------- test/unstake.ts | 34 +++++++++--------- 4 files changed, 97 insertions(+), 67 deletions(-) diff --git a/contracts/TokenGeyser.sol b/contracts/TokenGeyser.sol index ebfef11..1d07b15 100644 --- a/contracts/TokenGeyser.sol +++ b/contracts/TokenGeyser.sol @@ -62,6 +62,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; @@ -250,6 +251,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); @@ -260,7 +262,9 @@ contract TokenGeyser is rewardAmount = computeNewReward( rewardAmount, newStakingShareSecondsToBurn, - stakeTimeSec + totalStakingShareSeconds, + stakeTimeSec, + totalUnlocked_ ); stakingShareSecondsToBurn = stakingShareSecondsToBurn.add( newStakingShareSecondsToBurn @@ -273,7 +277,9 @@ contract TokenGeyser is rewardAmount = computeNewReward( rewardAmount, newStakingShareSecondsToBurn, - stakeTimeSec + totalStakingShareSeconds, + stakeTimeSec, + totalUnlocked_ ); stakingShareSecondsToBurn = stakingShareSecondsToBurn.add( newStakingShareSecondsToBurn @@ -320,29 +326,31 @@ 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)) - .mul(newRewardTokens) - .div(oneHundredPct); + uint256 bonusedReward = ( + BONUS_HUNDRED_PERC.sub(startBonus).mul(stakeTimeSec).div(bonusPeriodSec) + ).add(startBonus).mul(newRewardTokens).div(BONUS_HUNDRED_PERC); return currentRewardTokens.add(bonusedReward); } @@ -412,9 +420,9 @@ contract TokenGeyser is * @return durationSec The amount of time in seconds when all the reward tokens unlock. */ function unlockDuration() public view returns (uint256 durationSec) { - durationSec = type(uint256).max; + durationSec = 0; for (uint256 s = 0; s < unlockSchedules.length; s++) { - durationSec = Math.min( + durationSec = Math.max( (block.timestamp < unlockSchedules[s].endAtSec) ? unlockSchedules[s].endAtSec - block.timestamp : 0, @@ -467,12 +475,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) @@ -481,20 +490,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 ( @@ -502,7 +517,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 a05620f..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) @@ -530,14 +534,14 @@ describe("LockedPool", function () { 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 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; + const timeElapsed = t - _t + ONE_YEAR / 4; expect(r[5] - _r[5]) .to.gte(timeElapsed - 1) .to.lte(timeElapsed + 1); @@ -551,20 +555,25 @@ describe("LockedPool", function () { 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 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; + 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); + 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"); @@ -572,12 +581,13 @@ 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); }); }); }); describe("when user history does not exist", async function () { - describe("current state, with no additional stake", function(){ + 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(); @@ -591,9 +601,9 @@ describe("LockedPool", function () { .to.gte(timeElapsed - 1) .to.lte(timeElapsed + 1); }); - }) + }); - describe("current state, with additional stake", function(){ + 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(); @@ -607,39 +617,43 @@ describe("LockedPool", function () { .to.gte(timeElapsed - 1) .to.lte(timeElapsed + 1); }); - }) + }); - describe("after 3 months, without additional stake", function(){ + 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 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; + 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(){ + 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 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; + 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 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,