Skip to content

Commit

Permalink
refactor(RewardStreamerMP): extract MP and Stake mathematical formula…
Browse files Browse the repository at this point in the history
…s to abstract contracts
  • Loading branch information
3esmit authored and 0x-r4bbit committed Jan 30, 2025
1 parent 9807f49 commit a22da25
Show file tree
Hide file tree
Showing 10 changed files with 614 additions and 336 deletions.
147 changes: 60 additions & 87 deletions .gas-report

Large diffs are not rendered by default.

129 changes: 65 additions & 64 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,74 +1,75 @@
EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92734)
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 299008)
EmergencyExitTest:test_EmergencyExitBasic() (gas: 385662)
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 664160)
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 393691)
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 393030)
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 378630)
EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39470)
IntegrationTest:testStakeFoo() (gas: 1212157)
LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 5836880)
LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 296161)
LeaveTest:test_TrustNewStakeManager() (gas: 5907277)
LockTest:test_LockFailsWithInvalidPeriod() (gas: 311224)
LockTest:test_LockFailsWithNoStake() (gas: 63663)
LockTest:test_LockWithoutPriorLock() (gas: 390931)
MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1746581)
MathTest:test_CalcAbsoluteMaxTotalMP() (gas: 18995)
MathTest:test_CalcAccrueMP() (gas: 22229)
MathTest:test_CalcBonusMP() (gas: 17645)
MathTest:test_CalcInitialMP() (gas: 5330)
MathTest:test_CalcMaxAccruedMP() (gas: 15696)
MathTest:test_CalcMaxTotalMP() (gas: 23339)
MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 725540)
EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 92757)
EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 300544)
EmergencyExitTest:test_EmergencyExitBasic() (gas: 387340)
EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 667427)
EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 395139)
EmergencyExitTest:test_EmergencyExitWithLock() (gas: 394708)
EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 380241)
EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39471)
IntegrationTest:testStakeFoo() (gas: 1218594)
LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 6214173)
LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 297675)
LeaveTest:test_TrustNewStakeManager() (gas: 6269901)
LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1002, μ: 344783, ~: 344801)
LockTest:test_LockFailsWithNoStake() (gas: 102637)
LockTest:test_LockFailsWithZero() (gas: 315022)
LockTest:test_LockWithoutPriorLock() (gas: 393335)
MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1752531)
MathTest:test_CalcAbsoluteMaxTotalMP() (gas: 4996)
MathTest:test_CalcAccrueMP() (gas: 7990)
MathTest:test_CalcBonusMP() (gas: 18676)
MathTest:test_CalcInitialMP() (gas: 5352)
MathTest:test_CalcMaxAccruedMP() (gas: 4642)
MathTest:test_CalcMaxTotalMP() (gas: 19449)
MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 731369)
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: 486274)
RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 160637)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39317)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39340)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39375)
RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 618553)
StakeTest:test_StakeMultipleAccounts() (gas: 499457)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 505374)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 842563)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 515891)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 538001)
StakeTest:test_StakeOneAccount() (gas: 278207)
StakeTest:test_StakeOneAccountAndRewards() (gas: 284155)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 507692)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 499083)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298124)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 299768)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 299857)
RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 490632)
RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 160880)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39384)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39407)
RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39442)
RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 620722)
StakeTest:test_StakeMultipleAccounts() (gas: 502561)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 508596)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 847390)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 517705)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 539649)
StakeTest:test_StakeOneAccount() (gas: 279841)
StakeTest:test_StakeOneAccountAndRewards() (gas: 285896)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 510467)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 500009)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 300111)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 300696)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 300763)
StakingTokenTest:testStakeToken() (gas: 10422)
UnstakeTest:test_StakeMultipleAccounts() (gas: 499479)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 505396)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 842585)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 515935)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 537957)
UnstakeTest:test_StakeOneAccount() (gas: 278230)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 284133)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 507736)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 499040)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 298124)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 299768)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 299856)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 546251)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 704925)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 800718)
UnstakeTest:test_UnstakeOneAccount() (gas: 479941)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 502893)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 409031)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 531430)
UpgradeTest:test_RevertWhenNotOwner() (gas: 2709437)
UpgradeTest:test_UpgradeStakeManager() (gas: 5749376)
VaultRegistrationTest:test_VaultRegistration() (gas: 62017)
WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 311841)
UnstakeTest:test_StakeMultipleAccounts() (gas: 502560)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 508640)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 847367)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 517704)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 539693)
UnstakeTest:test_StakeOneAccount() (gas: 279841)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 285874)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 510511)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 500008)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 300111)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 300718)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 300762)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 546541)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 707663)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 803659)
UnstakeTest:test_UnstakeOneAccount() (gas: 481480)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 505028)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 410671)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 530083)
UpgradeTest:test_RevertWhenNotOwner() (gas: 2897740)
UpgradeTest:test_UpgradeStakeManager() (gas: 6114750)
VaultRegistrationTest:test_VaultRegistration() (gas: 62154)
WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 313397)
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
107 changes: 37 additions & 70 deletions src/RewardsStreamerMP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IStakeManager } from "./interfaces/IStakeManager.sol";
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,15 +19,16 @@ contract RewardsStreamerMP is
IStakeManager,
TrustedCodehashAccess,
ReentrancyGuardUpgradeable,
IRewardProvider
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();
Expand All @@ -36,12 +38,6 @@ contract RewardsStreamerMP is
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,30 @@ contract RewardsStreamerMP is
revert StakingManager__AmountCannotBeZero();
}

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

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

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

vault.stakedBalance += amount;
totalStaked += amount;

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

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

uint256 vaultMaxMP = initialMP + bonusMP + potentialMP;
uint256 vaultMP = initialMP + bonusMP;

vault.mpAccrued += vaultMP;
totalMPAccrued += vaultMP;
vault.mpAccrued += _deltaMpTotal;
totalMPAccrued += _deltaMpTotal;

vault.maxMP += vaultMaxMP;
totalMaxMP += vaultMaxMP;
vault.maxMP += _deltaMPMax;
totalMaxMP += _deltaMPMax;

vault.rewardIndex = rewardIndex;
}
Expand All @@ -238,33 +224,29 @@ contract RewardsStreamerMP is
onlyRegisteredVault
nonReentrant
{
if (lockPeriod < MIN_LOCKUP_PERIOD || lockPeriod > MAX_LOCKUP_PERIOD) {
revert StakingManager__InvalidLockingPeriod();
}

VaultData storage vault = vaultData[msg.sender];

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

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

_updateGlobalState();
_updateVaultMP(msg.sender, true);
(uint256 deltaMp, uint256 newLockEnd) =
_calculateLock(vault.stakedBalance, vault.maxMP, vault.lockUntil, block.timestamp, lockPeriod);

uint256 additionalBonusMP = _calculateBonusMP(vault.stakedBalance, lockPeriod);

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

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

vault.rewardIndex = rewardIndex;
}
Expand All @@ -277,32 +259,22 @@ contract RewardsStreamerMP is
nonReentrant
{
VaultData storage vault = vaultData[msg.sender];
if (amount > vault.stakedBalance) {
revert StakingManager__InsufficientBalance();
}

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

function _unstake(uint256 amount, VaultData storage vault, address vaultAddress) internal {
_updateGlobalState();
_updateVaultMP(vaultAddress, true);

uint256 previousStakedBalance = vault.stakedBalance;

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

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

Expand All @@ -315,6 +287,8 @@ contract RewardsStreamerMP is
VaultData storage vault = vaultData[msg.sender];

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

Expand Down Expand Up @@ -358,7 +332,7 @@ contract RewardsStreamerMP is
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 +439,19 @@ contract RewardsStreamerMP is
return (accruedRewards, newRewardIndex);
}

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

function _getVaultPendingMP(VaultData storage vault) internal view returns (uint256) {
if (vault.maxMP == 0 || vault.stakedBalance == 0) {
if (block.timestamp == vault.lastMPUpdateTime) {
return 0;
}

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

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

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

function _updateVaultMP(address vaultAddress, 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

0 comments on commit a22da25

Please sign in to comment.