From 7da364036ae8f2d91cbbfcfd82e526d50df03d63 Mon Sep 17 00:00:00 2001 From: itofarina Date: Wed, 14 Aug 2024 20:19:03 -0300 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20staking:=20use=20rate=20wi?= =?UTF-8?q?th=2018=20extra=20decimals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gas-snapshot | 154 ++++++++++++++++++++-------------------- contracts/StakedEXA.sol | 13 ++-- test/StakedEXA.t.sol | 38 +++++----- 3 files changed, 103 insertions(+), 102 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index f4c67d35..d591fe0a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -461,100 +461,100 @@ StakedEXATest:invariantNoDuplicatedReward() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:invariantRewardsUpOnly() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:invariantShareValueIsOne() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:testAlreadyListedError() (gas: 43818) -StakedEXATest:testAvgIndex(uint256[3],uint256[2]) (runs: 256, μ: 1249326, ~: 1280096) -StakedEXATest:testAvgStartTime(uint256[3],uint256[2]) (runs: 256, μ: 1235413, ~: 1266183) -StakedEXATest:testBalanceOfDeposit(uint80) (runs: 256, μ: 343934, ~: 350657) +StakedEXATest:testAvgIndex(uint256[3],uint256[2]) (runs: 256, μ: 1244940, ~: 1277402) +StakedEXATest:testAvgStartTime(uint256[3],uint256[2]) (runs: 256, μ: 1231468, ~: 1263930) +StakedEXATest:testBalanceOfDeposit(uint80) (runs: 256, μ: 344082, ~: 350809) StakedEXATest:testBalanceOfWithdraw(uint256) (runs: 256, μ: 60559, ~: 60566) -StakedEXATest:testCanChangeRewardsDurationWhenDisabled() (gas: 174255) -StakedEXATest:testClaimAfterHarvest() (gas: 854537) -StakedEXATest:testClaimAndUnstake() (gas: 1579981) -StakedEXATest:testClaimAndWithdrawAfterRefTime() (gas: 1078286) -StakedEXATest:testClaimBeforeFirstHarvest() (gas: 528369) -StakedEXATest:testDepositClaimsRewardsToReceiver() (gas: 1111826) -StakedEXATest:testDepositEvent(uint256) (runs: 256, μ: 352574, ~: 352309) -StakedEXATest:testDepositShouldClaim(uint256[2],uint32) (runs: 256, μ: 834811, ~: 758223) -StakedEXATest:testDepositToAnotherWithAllowance() (gas: 397241) +StakedEXATest:testCanChangeRewardsDurationWhenDisabled() (gas: 174316) +StakedEXATest:testClaimAfterHarvest() (gas: 852778) +StakedEXATest:testClaimAndUnstake() (gas: 1577054) +StakedEXATest:testClaimAndWithdrawAfterRefTime() (gas: 1075498) +StakedEXATest:testClaimBeforeFirstHarvest() (gas: 526753) +StakedEXATest:testDepositClaimsRewardsToReceiver() (gas: 1110595) +StakedEXATest:testDepositEvent(uint256) (runs: 256, μ: 352733, ~: 352461) +StakedEXATest:testDepositShouldClaim(uint256[2],uint32) (runs: 256, μ: 830795, ~: 757762) +StakedEXATest:testDepositToAnotherWithAllowance() (gas: 397393) StakedEXATest:testDepositToAnotherWithoutAllowanceShouldFail() (gas: 122801) -StakedEXATest:testDepositWithdrawAvgStartTimeAndIndex(uint256[3],uint256,uint256[5]) (runs: 256, μ: 1786870, ~: 1842082) -StakedEXATest:testEarnedWithTime(uint256) (runs: 256, μ: 35382, ~: 35668) -StakedEXATest:testEmergencyAdminCanPauseNotUnpause() (gas: 159215) -StakedEXATest:testFinishDistributionEmitEvent() (gas: 392727) -StakedEXATest:testFinishDistributionLetsClaimUnclaimed() (gas: 1590807) -StakedEXATest:testFinishDistributionStopsEmission() (gas: 1570525) -StakedEXATest:testFinishDistributionThatAlreadyFinished() (gas: 429609) -StakedEXATest:testFinishDistributionTransfersRemainingToSavings() (gas: 117495) +StakedEXATest:testDepositWithdrawAvgStartTimeAndIndex(uint256[3],uint256,uint256[5]) (runs: 256, μ: 1777448, ~: 1837028) +StakedEXATest:testEarnedWithTime(uint256) (runs: 256, μ: 35382, ~: 35660) +StakedEXATest:testEmergencyAdminCanPauseNotUnpause() (gas: 159219) +StakedEXATest:testFinishDistributionEmitEvent() (gas: 392940) +StakedEXATest:testFinishDistributionLetsClaimUnclaimed() (gas: 1585859) +StakedEXATest:testFinishDistributionStopsEmission() (gas: 1565879) +StakedEXATest:testFinishDistributionThatAlreadyFinished() (gas: 429614) +StakedEXATest:testFinishDistributionTransfersRemainingToSavings() (gas: 117624) StakedEXATest:testGrantRevokeEmergencyAdmin() (gas: 107572) StakedEXATest:testGrantRevokePauser() (gas: 107213) -StakedEXATest:testHandlerClaim(uint8) (runs: 256, μ: 302595, ~: 302595) -StakedEXATest:testHandlerDeposit(uint80) (runs: 256, μ: 812097, ~: 823337) -StakedEXATest:testHandlerHarvest(uint64) (runs: 256, μ: 332132, ~: 329784) -StakedEXATest:testHandlerNotifyRewardAmount(uint64) (runs: 256, μ: 127386, ~: 123288) -StakedEXATest:testHandlerSetDuration(uint32) (runs: 256, μ: 148067, ~: 169879) +StakedEXATest:testHandlerClaim(uint8) (runs: 256, μ: 302579, ~: 302579) +StakedEXATest:testHandlerDeposit(uint80) (runs: 256, μ: 810453, ~: 821717) +StakedEXATest:testHandlerHarvest(uint64) (runs: 256, μ: 332201, ~: 329940) +StakedEXATest:testHandlerNotifyRewardAmount(uint64) (runs: 256, μ: 131747, ~: 123606) +StakedEXATest:testHandlerSetDuration(uint32) (runs: 256, μ: 152397, ~: 170012) StakedEXATest:testHandlerSetMarket() (gas: 124946) StakedEXATest:testHandlerWithdraw(uint256) (runs: 256, μ: 70108, ~: 70115) -StakedEXATest:testHarvest() (gas: 185153) -StakedEXATest:testHarvestAmountWithReducedAllowance() (gas: 202987) -StakedEXATest:testHarvestEffectOnRewardData() (gas: 178155) -StakedEXATest:testHarvestEmitsRewardAmountNotified() (gas: 176174) -StakedEXATest:testHarvestFailDoesntDoSDeposits() (gas: 360531) -StakedEXATest:testHarvestWhenFinished() (gas: 308488) -StakedEXATest:testHarvestZero() (gas: 255193) -StakedEXATest:testInitialValues() (gas: 89226) -StakedEXATest:testInsufficientBalanceError(uint256) (runs: 256, μ: 64191, ~: 64309) -StakedEXATest:testMaxRewardsGasConsumption() (gas: 138524347) -StakedEXATest:testMultipleClaimsVsOne() (gas: 25445352) -StakedEXATest:testMultipleHarvests() (gas: 436412) -StakedEXATest:testNoRewardsAfterPeriod(uint256) (runs: 256, μ: 1581769, ~: 1588378) +StakedEXATest:testHarvest() (gas: 185317) +StakedEXATest:testHarvestAmountWithReducedAllowance() (gas: 203143) +StakedEXATest:testHarvestEffectOnRewardData() (gas: 178234) +StakedEXATest:testHarvestEmitsRewardAmountNotified() (gas: 176330) +StakedEXATest:testHarvestFailDoesntDoSDeposits() (gas: 360622) +StakedEXATest:testHarvestWhenFinished() (gas: 308705) +StakedEXATest:testHarvestZero() (gas: 255500) +StakedEXATest:testInitialValues() (gas: 89426) +StakedEXATest:testInsufficientBalanceError(uint256) (runs: 256, μ: 64345, ~: 64464) +StakedEXATest:testMaxRewardsGasConsumption() (gas: 138480291) +StakedEXATest:testMultipleClaimsVsOne() (gas: 25386623) +StakedEXATest:testMultipleHarvests() (gas: 436723) +StakedEXATest:testNoRewardsAfterPeriod(uint256) (runs: 256, μ: 1577370, ~: 1583971) StakedEXATest:testNotPausingRoleError() (gas: 39589) -StakedEXATest:testNotifyRewardAmount(uint256,uint256) (runs: 256, μ: 129975, ~: 129907) -StakedEXATest:testNotifyRewardWithUnderlyingAsset() (gas: 485748) -StakedEXATest:testOnlyAdminEnableReward() (gas: 1198990) -StakedEXATest:testOnlyAdminFinishDistribution() (gas: 196446) -StakedEXATest:testOnlyAdminNotifyRewardAmount() (gas: 203421) -StakedEXATest:testOnlyAdminSetProvider() (gas: 143580) -StakedEXATest:testOnlyAdminSetProviderRatio() (gas: 143273) -StakedEXATest:testOnlyAdminSetRewardsDuration() (gas: 152933) -StakedEXATest:testOnlyAdminSetSavings() (gas: 141099) -StakedEXATest:testPausable() (gas: 1169123) -StakedEXATest:testPausableClaim() (gas: 627637) -StakedEXATest:testPausableHarvest() (gas: 331855) +StakedEXATest:testNotifyRewardAmount(uint256,uint256) (runs: 256, μ: 130216, ~: 130153) +StakedEXATest:testNotifyRewardWithUnderlyingAsset() (gas: 485916) +StakedEXATest:testOnlyAdminEnableReward() (gas: 1198994) +StakedEXATest:testOnlyAdminFinishDistribution() (gas: 196511) +StakedEXATest:testOnlyAdminNotifyRewardAmount() (gas: 203580) +StakedEXATest:testOnlyAdminSetProvider() (gas: 143584) +StakedEXATest:testOnlyAdminSetProviderRatio() (gas: 143277) +StakedEXATest:testOnlyAdminSetRewardsDuration() (gas: 152937) +StakedEXATest:testOnlyAdminSetSavings() (gas: 141103) +StakedEXATest:testPausable() (gas: 1165167) +StakedEXATest:testPausableClaim() (gas: 627789) +StakedEXATest:testPausableHarvest() (gas: 332007) StakedEXATest:testPauserCanPauseUnpause() (gas: 157853) StakedEXATest:testPenaltyGrowthRange() (gas: 67211) StakedEXATest:testPenaltyThresholdRange() (gas: 37112) -StakedEXATest:testPermitAndDeposit() (gas: 424117) -StakedEXATest:testPermitFailKeepsFlow() (gas: 473086) -StakedEXATest:testRemoveDepositAllowance() (gas: 489720) -StakedEXATest:testResetDepositAfterRefTime(uint256) (runs: 256, μ: 1028086, ~: 1027837) -StakedEXATest:testResetStake() (gas: 1017878) -StakedEXATest:testRewardAmountNotifiedEvent(uint256) (runs: 256, μ: 104677, ~: 105546) +StakedEXATest:testPermitAndDeposit() (gas: 424269) +StakedEXATest:testPermitFailKeepsFlow() (gas: 473238) +StakedEXATest:testRemoveDepositAllowance() (gas: 489872) +StakedEXATest:testResetDepositAfterRefTime(uint256) (runs: 256, μ: 1026278, ~: 1026022) +StakedEXATest:testResetStake() (gas: 1016206) +StakedEXATest:testRewardAmountNotifiedEvent(uint256) (runs: 256, μ: 106008, ~: 105737) StakedEXATest:testRewardNotListedError() (gas: 1109543) -StakedEXATest:testRewardPaidEvent(uint256,uint256) (runs: 256, μ: 802085, ~: 851002) -StakedEXATest:testRewardsAmounts(uint256) (runs: 256, μ: 1589330, ~: 1588999) -StakedEXATest:testRewardsDurationSetEvent(uint40) (runs: 256, μ: 52074, ~: 52063) -StakedEXATest:testSetDuration(uint256,uint40) (runs: 256, μ: 58951, ~: 59204) +StakedEXATest:testRewardPaidEvent(uint256,uint256) (runs: 256, μ: 801843, ~: 860844) +StakedEXATest:testRewardsAmounts(uint256) (runs: 256, μ: 1584850, ~: 1584553) +StakedEXATest:testRewardsDurationSetEvent(uint40) (runs: 256, μ: 52075, ~: 52057) +StakedEXATest:testSetDuration(uint256,uint40) (runs: 256, μ: 58953, ~: 59204) StakedEXATest:testSetMarketAddressZero() (gas: 37102) -StakedEXATest:testSetMarketOnlyAdmin() (gas: 1272533) +StakedEXATest:testSetMarketOnlyAdmin() (gas: 1272537) StakedEXATest:testSetMaxRewardsTokensExceeded() (gas: 104944739) -StakedEXATest:testSetMinTime() (gas: 82094) -StakedEXATest:testSetPenaltyGrowth() (gas: 82192) -StakedEXATest:testSetPenaltyThreshold() (gas: 81970) +StakedEXATest:testSetMinTime() (gas: 82098) +StakedEXATest:testSetPenaltyGrowth() (gas: 82196) +StakedEXATest:testSetPenaltyThreshold() (gas: 81974) StakedEXATest:testSetProviderRatioOverOneError() (gas: 37135) StakedEXATest:testSetSavingsZeroAddressError() (gas: 37196) -StakedEXATest:testTotalSupplyDeposit(uint80) (runs: 256, μ: 343471, ~: 350194) +StakedEXATest:testTotalSupplyDeposit(uint80) (runs: 256, μ: 343619, ~: 350346) StakedEXATest:testTotalSupplyWithdraw(uint256) (runs: 256, μ: 62073, ~: 62080) -StakedEXATest:testUntransferable(uint80) (runs: 256, μ: 363144, ~: 370768) -StakedEXATest:testWithdrawEvent(uint256) (runs: 256, μ: 508865, ~: 508612) -StakedEXATest:testWithdrawRewardUnderlyingAsset() (gas: 466586) -StakedEXATest:testWithdrawRewardsOnlyAdmin() (gas: 227918) +StakedEXATest:testUntransferable(uint80) (runs: 256, μ: 363292, ~: 370920) +StakedEXATest:testWithdrawEvent(uint256) (runs: 256, μ: 507260, ~: 507000) +StakedEXATest:testWithdrawRewardUnderlyingAsset() (gas: 466738) +StakedEXATest:testWithdrawRewardsOnlyAdmin() (gas: 227922) StakedEXATest:testWithdrawRewardsOnlyReward() (gas: 1109561) -StakedEXATest:testWithdrawSameAmountRewardsShouldEqual(uint256,uint256) (runs: 256, μ: 1065379, ~: 1132181) -StakedEXATest:testWithdrawWithRewards(uint256) (runs: 256, μ: 868194, ~: 867941) -StakedEXATest:testZeroRateError() (gas: 58048) -StakingPreviewerTest:testAllClaimable() (gas: 452415) -StakingPreviewerTest:testAllClaimed() (gas: 655167) -StakingPreviewerTest:testAllEarned() (gas: 338489) -StakingPreviewerTest:testAllRewards() (gas: 507362) -StakingPreviewerTest:testStaking() (gas: 558429) +StakedEXATest:testWithdrawSameAmountRewardsShouldEqual(uint256,uint256) (runs: 256, μ: 1064808, ~: 1128805) +StakedEXATest:testWithdrawWithRewards(uint256) (runs: 256, μ: 866446, ~: 866186) +StakedEXATest:testZeroRateError() (gas: 58139) +StakingPreviewerTest:testAllClaimable() (gas: 451330) +StakingPreviewerTest:testAllClaimed() (gas: 654670) +StakingPreviewerTest:testAllEarned() (gas: 337404) +StakingPreviewerTest:testAllRewards() (gas: 505101) +StakingPreviewerTest:testStaking() (gas: 556168) SwapperTest:testSwapBasic() (gas: 216831) SwapperTest:testSwapWithAllowance() (gas: 481530) SwapperTest:testSwapWithInaccurateSlippageSendsETHToAccount() (gas: 297968) diff --git a/contracts/StakedEXA.sol b/contracts/StakedEXA.sol index 81ee6302..6dca00d6 100644 --- a/contracts/StakedEXA.sol +++ b/contracts/StakedEXA.sol @@ -221,15 +221,15 @@ contract StakedEXA is updateIndex(reward); RewardData storage rewardData = rewards[reward]; if (block.timestamp >= rewardData.finishAt) { - rewardData.rate = amount / rewardData.duration; + rewardData.rate = (amount * 1e18) / rewardData.duration; } else { uint256 remainingRewards = (rewardData.finishAt - block.timestamp) * rewardData.rate; - rewardData.rate = (amount + remainingRewards) / rewardData.duration; + rewardData.rate = (amount * 1e18 + remainingRewards) / rewardData.duration; } if (rewardData.rate == 0) revert ZeroRate(); if ( - rewardData.rate * rewardData.duration > + rewardData.rate.mulWadDown(rewardData.duration) > reward.balanceOf(address(this)) - (address(reward) == asset() ? totalAssets() : 0) ) revert InsufficientBalance(); @@ -297,9 +297,7 @@ contract StakedEXA is return rewardData.index + - (rewardData.rate * (lastTimeRewardApplicable(rewardData.finishAt) - rewardData.updatedAt)).divWadDown( - totalSupply() - ); + rewardData.rate.mulDivDown(lastTimeRewardApplicable(rewardData.finishAt) - rewardData.updatedAt, totalSupply()); } /// @notice Returns the average index for a reward token and account. @@ -446,7 +444,7 @@ contract StakedEXA is if (block.timestamp < rewards[reward].finishAt) { uint256 finishAt = rewards[reward].finishAt; rewards[reward].finishAt = uint40(block.timestamp); - reward.safeTransfer(savings, (finishAt - block.timestamp) * rewards[reward].rate); + reward.safeTransfer(savings, (finishAt - block.timestamp).mulWadDown(rewards[reward].rate)); } if (reward == IERC20(address(market.asset()))) setProvider(address(0)); @@ -614,6 +612,7 @@ struct RewardData { uint40 finishAt; uint40 updatedAt; uint256 index; + /// @notice rate in assets per second with 18 extra decimals uint256 rate; } diff --git a/test/StakedEXA.t.sol b/test/StakedEXA.t.sol index eb263ec9..069e7d7a 100644 --- a/test/StakedEXA.t.sol +++ b/test/StakedEXA.t.sol @@ -447,7 +447,9 @@ contract StakedEXATest is Test { if (rDuration == 0) vm.expectRevert(stdError.divisionError); else if ( ( - block.timestamp >= finishAt ? assets / rDuration : (assets + ((finishAt - block.timestamp) * rate)) / rDuration + block.timestamp >= finishAt + ? (uint256(assets) * 1e18) / rDuration + : (uint256(assets) * 1e18 + (finishAt - block.timestamp) * rate) / rDuration ) == 0 ) vm.expectRevert(ZeroRate.selector); stEXA.notifyRewardAmount(reward, assets); @@ -462,7 +464,7 @@ contract StakedEXATest is Test { (, uint40 finishAt, , , uint256 rate) = stEXA.rewards(reward); if (finishAt > block.timestamp) { - uint256 remainingRewards = rate * (finishAt - block.timestamp); + uint256 remainingRewards = rate.mulWadDown(finishAt - block.timestamp); stEXA.finishDistribution(reward); assertEq(reward.balanceOf(SAVINGS), savingsBalance + remainingRewards, "missing remaining savings"); @@ -494,7 +496,7 @@ contract StakedEXATest is Test { assertEq(duration0, duration); assertEq(finishAt0, block.timestamp + duration); assertEq(index0, 0); - assertEq(rate0, initialAmount / duration); + assertEq(rate0, (initialAmount * 1e18) / duration); assertEq(updatedAt0, block.timestamp); (uint256 duration1, uint256 finishAt1, uint256 updatedAt1, uint256 index1, uint256 rate1) = stEXA.rewards(rB); @@ -502,7 +504,7 @@ contract StakedEXATest is Test { assertEq(duration1, duration); assertEq(finishAt1, block.timestamp + duration); assertEq(index1, 0); - assertEq(rate1, initialAmount / duration); + assertEq(rate1, (initialAmount * 1e18) / duration); assertEq(updatedAt1, block.timestamp); assertEq(stEXA.totalSupply(), 0); @@ -615,9 +617,9 @@ contract StakedEXATest is Test { exa.mint(address(this), assets); stEXA.deposit(assets, address(this)); - uint256 rate = initialAmount / duration; + uint256 rate = (initialAmount * 1e18) / duration; skip(duration / 2); - uint256 earned_ = rate * (duration / 2); + uint256 earned_ = rate.mulWadDown(duration / 2); assertApproxEqAbs(earned(rA, address(this)), earned_, 2e6, "earned != expected"); uint256 thisClaimable = claimable(rA, address(this)); @@ -691,9 +693,9 @@ contract StakedEXATest is Test { uint256 expectedRate = 0; if (block.timestamp >= finishAt) { - expectedRate = amount / duration; + expectedRate = (amount * 1e18) / duration; } else { - expectedRate = (amount + (finishAt - block.timestamp) * rate) / duration; + expectedRate = ((amount * 1e18) + (finishAt - block.timestamp) * rate) / duration; } rA.mint(address(stEXA), amount); @@ -761,7 +763,7 @@ contract StakedEXATest is Test { stEXA.deposit(assets, address(this)); skip(time); - uint256 thisRewards = rate * time; + uint256 thisRewards = rate.mulWadDown(time); exa.mint(BOB, assets); vm.startPrank(BOB); @@ -771,7 +773,7 @@ contract StakedEXATest is Test { skip(time); - uint256 bobRewards = (rate * time) / 2; + uint256 bobRewards = rate.mulWadDown(time) / 2; thisRewards += bobRewards; assertApproxEqAbs(earned(rA, address(this)), thisRewards, 1e7, "this rewards != earned expected"); @@ -795,13 +797,13 @@ contract StakedEXATest is Test { uint256 assets = 1_000e18; uint256 time = duration / 2; - uint256 rate = initialAmount / duration; + uint256 rate = (initialAmount * 1e18) / duration; exa.mint(address(this), assets); stEXA.deposit(assets, address(this)); skip(time); - uint256 thisRewards = rate * time; + uint256 thisRewards = rate.mulWadDown(time); exa.mint(BOB, assets); vm.startPrank(BOB); @@ -811,11 +813,11 @@ contract StakedEXATest is Test { skip(time); - uint256 bobRewards = (rate * time) / 2; + uint256 bobRewards = rate.mulWadDown(time / 2); thisRewards += bobRewards; - assertApproxEqAbs(earned(rA, address(this)), thisRewards, 600, "this rewards != earned expected"); - assertApproxEqAbs(earned(rA, BOB), bobRewards, 200, "bob rewards != earned expected"); + assertApproxEqAbs(earned(rA, address(this)), thisRewards, 2e3, "this rewards != earned expected"); + assertApproxEqAbs(earned(rA, BOB), bobRewards, 1e3, "bob rewards != earned expected"); skip(timeAfterPeriod); @@ -1122,7 +1124,7 @@ contract StakedEXATest is Test { assertEq(providerDuration, 1 weeks); assertEq(finishAt, block.timestamp + 1 weeks); assertEq(index, 0); - assertEq(rate, assets.mulWadDown(providerRatio) / 1 weeks); + assertEq(rate, (assets * providerRatio) / 1 weeks); assertEq(updatedAt, block.timestamp); } @@ -1300,7 +1302,7 @@ contract StakedEXATest is Test { uint256 savingsBalance = rA.balanceOf(SAVINGS); (, uint256 finishAt, , , uint256 rate) = stEXA.rewards(rA); - uint256 remainingRewards = rate * (finishAt - block.timestamp); + uint256 remainingRewards = rate.mulWadDown(finishAt - block.timestamp); stEXA.finishDistribution(rA); assertEq(rA.balanceOf(SAVINGS), savingsBalance + remainingRewards); @@ -1319,7 +1321,7 @@ contract StakedEXATest is Test { (, uint256 finishAt, , , uint256 rate) = stEXA.rewards(rA); - uint256 remainingRewards = finishAt > block.timestamp ? rate * (finishAt - block.timestamp) : 0; + uint256 remainingRewards = finishAt > block.timestamp ? rate.mulWadDown(finishAt - block.timestamp) : 0; assertEq(remainingRewards, 0);