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

refactor(RewardStreamerMP): extract MP and Stake mathematical formula… #109

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
147 changes: 60 additions & 87 deletions .gas-report

Large diffs are not rendered by default.

123 changes: 62 additions & 61 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,74 +1,75 @@
EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92690)
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 299030)
EmergencyExitTest:test_EmergencyExitBasic() (gas: 385790)
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 664369)
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 393709)
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 393097)
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 378651)
EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39426)
IntegrationTest:testStakeFoo() (gas: 1212612)
LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 5836926)
LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 296161)
LeaveTest:test_TrustNewStakeManager() (gas: 5907344)
LockTest:test_LockFailsWithInvalidPeriod() (gas: 311246)
LockTest:test_LockFailsWithNoStake() (gas: 63730)
LockTest:test_LockWithoutPriorLock() (gas: 390945)
MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1746668)
MathTest:test_CalcAbsoluteMaxTotalMP() (gas: 18908)
MathTest:test_CalcAccrueMP() (gas: 22207)
MathTest:test_CalcBonusMP() (gas: 17713)
MathTest:test_CalcInitialMP() (gas: 5395)
MathTest:test_CalcMaxAccruedMP() (gas: 15586)
MathTest:test_CalcMaxTotalMP() (gas: 23298)
MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 725628)
EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92713)
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 301005)
EmergencyExitTest:test_EmergencyExitBasic() (gas: 387819)
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 668429)
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 395663)
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 395213)
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 380725)
EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39427)
IntegrationTest:testStakeFoo() (gas: 1220773)
LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 6346366)
LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 298092)
LeaveTest:test_TrustNewStakeManager() (gas: 6402009)
LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1002, μ: 345225, ~: 345252)
LockTest:test_LockFailsWithNoStake() (gas: 102637)
LockTest:test_LockFailsWithZero() (gas: 315395)
LockTest:test_LockWithoutPriorLock() (gas: 393897)
MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1752948)
MathTest:test_CalcAbsoluteMaxTotalMP() (gas: 4974)
MathTest:test_CalcAccrueMP() (gas: 8033)
MathTest:test_CalcBonusMP() (gas: 18610)
MathTest:test_CalcInitialMP() (gas: 5330)
MathTest:test_CalcMaxAccruedMP() (gas: 4665)
MathTest:test_CalcMaxTotalMP() (gas: 19383)
MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 732731)
NFTMetadataGeneratorSVGTest:testGenerateMetadata() (gas: 85934)
NFTMetadataGeneratorSVGTest:testSetImageStrings() (gas: 58332)
NFTMetadataGeneratorSVGTest:testSetImageStringsRevert() (gas: 35804)
NFTMetadataGeneratorURLTest:testGenerateMetadata() (gas: 102512)
NFTMetadataGeneratorURLTest:testSetBaseURL() (gas: 49555)
NFTMetadataGeneratorURLTest:testSetBaseURLRevert() (gas: 35979)
RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 486296)
RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 491205)
RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 160703)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39404)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39317)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39340)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39375)
RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 618531)
StakeTest:test_StakeMultipleAccounts() (gas: 499536)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 505474)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 842540)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 515936)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 537979)
StakeTest:test_StakeOneAccount() (gas: 278268)
StakeTest:test_StakeOneAccountAndRewards() (gas: 284215)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 507661)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 499052)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298123)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 299812)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 299879)
RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 620983)
StakeTest:test_StakeMultipleAccounts() (gas: 503497)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 509532)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 848506)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 518539)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 540506)
StakeTest:test_StakeOneAccount() (gas: 280320)
StakeTest:test_StakeOneAccountAndRewards() (gas: 286353)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 510997)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 500558)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 300551)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 301113)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 301224)
StakingTokenTest:testStakeToken() (gas: 10422)
UnstakeTest:test_StakeMultipleAccounts() (gas: 499558)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 505496)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 842562)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 515935)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 538001)
UnstakeTest:test_StakeOneAccount() (gas: 278291)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 284259)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 507705)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 499054)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298123)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 299812)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 299878)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 546326)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 705083)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 800935)
UnstakeTest:test_UnstakeOneAccount() (gas: 480104)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 502976)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 409152)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 531453)
UpgradeTest:test_RevertWhenNotOwner() (gas: 2709437)
UpgradeTest:test_UpgradeStakeManager() (gas: 5749483)
VaultRegistrationTest:test_VaultRegistration() (gas: 62013)
WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 311863)
UnstakeTest:test_StakeMultipleAccounts() (gas: 503496)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 509576)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 848483)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 518538)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 540505)
UnstakeTest:test_StakeOneAccount() (gas: 280320)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 286397)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 510996)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 500560)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 300573)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 301135)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 301224)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 547570)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 709361)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 805452)
UnstakeTest:test_UnstakeOneAccount() (gas: 482585)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 505999)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 411721)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 530970)
UpgradeTest:test_RevertWhenNotOwner() (gas: 2965589)
UpgradeTest:test_UpgradeStakeManager() (gas: 6246793)
VaultRegistrationTest:test_VaultRegistration() (gas: 62058)
WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 313836)
XPNFTTokenTest:testApproveNotAllowed() (gas: 10500)
XPNFTTokenTest:testGetApproved() (gas: 10523)
XPNFTTokenTest:testIsApprovedForAll() (gas: 10698)
Expand Down
7 changes: 6 additions & 1 deletion certora/specs/EmergencyMode.spec
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ definition isViewFunction(method f) returns bool = (
f.selector == sig:streamer.YEAR().selector ||
f.selector == sig:streamer.STAKING_TOKEN().selector ||
f.selector == sig:streamer.SCALE_FACTOR().selector ||
f.selector == sig:streamer.MP_RATE_PER_YEAR().selector ||
f.selector == sig:streamer.MP_APY().selector ||
f.selector == sig:streamer.MP_MPY().selector ||
f.selector == sig:streamer.MP_MPY_ABSOLUTE().selector ||
f.selector == sig:streamer.ACCRUE_RATE().selector ||
f.selector == sig:streamer.MIN_BALANCE().selector ||
f.selector == sig:streamer.MAX_BALANCE().selector ||
f.selector == sig:streamer.MIN_LOCKUP_PERIOD().selector ||
f.selector == sig:streamer.MAX_LOCKUP_PERIOD().selector ||
f.selector == sig:streamer.MAX_MULTIPLIER().selector ||
Expand Down
106 changes: 37 additions & 69 deletions src/RewardsStreamerMP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { IStakeVault } from "./interfaces/IStakeVault.sol";
import { IRewardProvider } from "./interfaces/IRewardProvider.sol";
import { TrustedCodehashAccess } from "./TrustedCodehashAccess.sol";
import { StakeMath } from "./math/StakeMath.sol";

// Rewards Streamer with Multiplier Points
contract RewardsStreamerMP is
Expand All @@ -18,30 +19,25 @@
IStakeManager,
TrustedCodehashAccess,
ReentrancyGuardUpgradeable,
IRewardProvider
3esmit marked this conversation as resolved.
Show resolved Hide resolved
IRewardProvider,
StakeMath
{
error StakingManager__InvalidVault();
error StakingManager__VaultNotRegistered();
error StakingManager__VaultAlreadyRegistered();
error StakingManager__AmountCannotBeZero();
error StakingManager__TransferFailed();
error StakingManager__InsufficientBalance();
error StakingManager__InvalidLockingPeriod();
error StakingManager__LockingPeriodCannotBeZero();
error StakingManager__CannotRestakeWithLockedFunds();
error StakingManager__TokensAreLocked();
error StakingManager__AlreadyLocked();
error StakingManager__EmergencyModeEnabled();
error StakingManager__DurationCannotBeZero();

IERC20 public STAKING_TOKEN;

Check warning on line 38 in src/RewardsStreamerMP.sol

View workflow job for this annotation

GitHub Actions / lint

Variable name must be in mixedCase

uint256 public constant SCALE_FACTOR = 1e18;
uint256 public constant MP_RATE_PER_YEAR = 1;

uint256 public constant YEAR = 365 days;
uint256 public constant MIN_LOCKUP_PERIOD = 90 days;
uint256 public constant MAX_LOCKUP_PERIOD = 4 * YEAR;
uint256 public constant MAX_MULTIPLIER = 4;

uint256 public totalStaked;
uint256 public totalMPAccrued;
Expand Down Expand Up @@ -193,40 +189,31 @@
revert StakingManager__AmountCannotBeZero();
}

if (lockPeriod != 0 && (lockPeriod < MIN_LOCKUP_PERIOD || lockPeriod > MAX_LOCKUP_PERIOD)) {
revert StakingManager__InvalidLockingPeriod();
}

_updateGlobalState();
_updateAccountMP(msg.sender, true);

Account storage account = accounts[msg.sender];
if (account.lockUntil != 0 && account.lockUntil > block.timestamp) {
revert StakingManager__CannotRestakeWithLockedFunds();
}
(uint256 _deltaMpTotal, uint256 _deltaMPMax, uint256 _newLockEnd) = _calculateStake(
account.stakedBalance, account.maxMP, account.lockUntil, block.timestamp, amount, lockPeriod
);

account.stakedBalance += amount;
totalStaked += amount;

uint256 initialMP = amount;
uint256 potentialMP = amount * MAX_MULTIPLIER;
uint256 bonusMP = 0;

if (lockPeriod != 0) {
bonusMP = _calculateBonusMP(amount, lockPeriod);
account.lockUntil = block.timestamp + lockPeriod;
account.lockUntil = _newLockEnd;
} else {
account.lockUntil = 0;
}

uint256 accountMaxMP = initialMP + bonusMP + potentialMP;
uint256 accountMP = initialMP + bonusMP;

account.mpAccrued += accountMP;
totalMPAccrued += accountMP;
account.mpAccrued += _deltaMpTotal;
totalMPAccrued += _deltaMpTotal;

account.maxMP += accountMaxMP;
totalMaxMP += accountMaxMP;
account.maxMP += _deltaMPMax;
totalMaxMP += _deltaMPMax;

account.accountRewardIndex = rewardIndex;
}
Expand All @@ -238,33 +225,29 @@
onlyRegisteredVault
nonReentrant
{
if (lockPeriod < MIN_LOCKUP_PERIOD || lockPeriod > MAX_LOCKUP_PERIOD) {
revert StakingManager__InvalidLockingPeriod();
}

Account storage account = accounts[msg.sender];

if (account.lockUntil > 0) {
revert StakingManager__AlreadyLocked();
}

if (account.stakedBalance == 0) {
revert StakingManager__InsufficientBalance();
if (lockPeriod == 0) {
revert StakingManager__LockingPeriodCannotBeZero();
}

_updateGlobalState();
_updateAccountMP(msg.sender, true);

uint256 additionalBonusMP = _calculateBonusMP(account.stakedBalance, lockPeriod);
(uint256 deltaMp, uint256 newLockEnd) =
_calculateLock(account.stakedBalance, account.maxMP, account.lockUntil, block.timestamp, lockPeriod);

// Update account state
account.lockUntil = block.timestamp + lockPeriod;
account.mpAccrued += additionalBonusMP;
account.maxMP += additionalBonusMP;
account.lockUntil = newLockEnd;
account.mpAccrued += deltaMp;
account.maxMP += deltaMp;

// Update global state
totalMPAccrued += additionalBonusMP;
totalMaxMP += additionalBonusMP;
totalMPAccrued += deltaMp;
totalMaxMP += deltaMp;

account.accountRewardIndex = rewardIndex;
}
Expand All @@ -277,32 +260,22 @@
nonReentrant
{
Account storage account = accounts[msg.sender];
if (amount > account.stakedBalance) {
revert StakingManager__InsufficientBalance();
}

if (block.timestamp < account.lockUntil) {
revert StakingManager__TokensAreLocked();
}
_unstake(amount, account, msg.sender);
}

function _unstake(uint256 amount, Account storage account, address accountAddress) internal {
_updateGlobalState();
_updateAccountMP(accountAddress, true);

uint256 previousStakedBalance = account.stakedBalance;

// solhint-disable-next-line
uint256 mpToReduce = Math.mulDiv(account.mpAccrued, amount, previousStakedBalance);
uint256 maxMPToReduce = Math.mulDiv(account.maxMP, amount, previousStakedBalance);

(uint256 _deltaMpTotal, uint256 _deltaMpMax) = _calculateUnstake(
account.stakedBalance, account.lockUntil, block.timestamp, account.mpAccrued, account.maxMP, amount
);
account.stakedBalance -= amount;
account.mpAccrued -= mpToReduce;
account.maxMP -= maxMPToReduce;
account.mpAccrued -= _deltaMpTotal;
account.maxMP -= _deltaMpMax;
account.accountRewardIndex = rewardIndex;
totalMPAccrued -= mpToReduce;
totalMaxMP -= maxMPToReduce;
totalMPAccrued -= _deltaMpTotal;
totalMaxMP -= _deltaMpMax;
totalStaked -= amount;
}

Expand All @@ -315,6 +288,8 @@
Account storage account = accounts[msg.sender];

if (account.stakedBalance > 0) {
//updates lockuntil to allow unstake early
account.lockUntil = block.timestamp;
// calling `_unstake` to update accounting accordingly
_unstake(account.stakedBalance, account, msg.sender);

Expand Down Expand Up @@ -358,7 +333,7 @@
return (adjustedRewardIndex, totalMPAccrued);
}

uint256 accruedMP = (timeDiff * totalStaked * MP_RATE_PER_YEAR) / YEAR;
uint256 accruedMP = _accrueMP(totalStaked, timeDiff);
if (totalMPAccrued + accruedMP > totalMaxMP) {
accruedMP = totalMaxMP - totalMPAccrued;
}
Expand Down Expand Up @@ -465,26 +440,19 @@
return (accruedRewards, newRewardIndex);
}

function _calculateBonusMP(uint256 amount, uint256 lockPeriod) internal pure returns (uint256) {
return Math.mulDiv(amount, lockPeriod, YEAR);
}

function _getAccountPendingdMP(Account storage account) internal view returns (uint256) {
if (account.maxMP == 0 || account.stakedBalance == 0) {
if (block.timestamp == account.lastMPUpdateTime) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one

return 0;
}

uint256 timeDiff = block.timestamp - account.lastMPUpdateTime;
if (timeDiff == 0) {
if (account.maxMP == 0 || account.stakedBalance == 0) {
return 0;
}

uint256 accruedMP = Math.mulDiv(timeDiff * account.stakedBalance, MP_RATE_PER_YEAR, YEAR);
uint256 deltaMpTotal = _calculateAccrual(
account.stakedBalance, account.mpAccrued, account.maxMP, account.lastMPUpdateTime, block.timestamp
);

if (account.mpAccrued + accruedMP > account.maxMP) {
accruedMP = account.maxMP - account.mpAccrued;
}
return accruedMP;
return deltaMpTotal;
}

function _updateAccountMP(address accountAddress, bool forceMPUpdate) internal {
Expand Down
17 changes: 17 additions & 0 deletions src/interfaces/IStakeConstants.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import { ITrustedCodehashAccess } from "./ITrustedCodehashAccess.sol";

Check warning on line 4 in src/interfaces/IStakeConstants.sol

View workflow job for this annotation

GitHub Actions / lint

imported name ITrustedCodehashAccess is not used

/**
* @title IStakeConstants
* @author Ricardo Guilherme Schmidt <ricardo3@status.im>
* @notice Interface for Stake Constants
* @dev This interface is necessary to linearize the inheritance of StakeMath and MultiplierPointMath
*/
interface IStakeConstants {
function MIN_LOCKUP_PERIOD() external view returns (uint256);
function MAX_LOCKUP_PERIOD() external view returns (uint256);
function MP_APY() external view returns (uint256);
function MAX_MULTIPLIER() external view returns (uint256);
}
Loading
Loading