Skip to content

Commit

Permalink
feat(RewardsStreamerMP): add lock(uint256) function
Browse files Browse the repository at this point in the history
This adds a `lock()` function to the staking contract, allowing vaults
to lock their funds after they have staked in the past.

Closes #40
  • Loading branch information
0x-r4bbit committed Nov 1, 2024
1 parent 91cd844 commit 5bc7ebf
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 53 deletions.
37 changes: 19 additions & 18 deletions .gas-report
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,33 @@
| src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | |
|------------------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 1216069 | 5542 | | | | |
| 1327847 | 6066 | | | | |
| Function Name | min | avg | median | max | # calls |
| MAX_LOCKUP_PERIOD | 228 | 228 | 228 | 228 | 22 |
| MAX_MULTIPLIER | 229 | 229 | 229 | 229 | 28 |
| MAX_LOCKUP_PERIOD | 228 | 228 | 228 | 228 | 23 |
| MAX_MULTIPLIER | 274 | 274 | 274 | 274 | 30 |
| MIN_LOCKUP_PERIOD | 275 | 275 | 275 | 275 | 11 |
| MP_RATE_PER_YEAR | 231 | 231 | 231 | 231 | 3 |
| SCALE_FACTOR | 229 | 229 | 229 | 229 | 39 |
| STAKING_TOKEN | 273 | 273 | 273 | 273 | 128 |
| accountedRewards | 395 | 953 | 395 | 2395 | 68 |
| getAccount | 1596 | 1596 | 1596 | 1596 | 65 |
| isTrustedCodehash | 496 | 996 | 496 | 2496 | 128 |
| SCALE_FACTOR | 295 | 295 | 295 | 295 | 41 |
| STAKING_TOKEN | 273 | 273 | 273 | 273 | 140 |
| accountedRewards | 373 | 931 | 373 | 2373 | 68 |
| getAccount | 1596 | 1596 | 1596 | 1596 | 67 |
| isTrustedCodehash | 496 | 996 | 496 | 2496 | 140 |
| rewardIndex | 351 | 380 | 351 | 2351 | 68 |
| setTrustedCodehash | 47926 | 47926 | 47926 | 47926 | 32 |
| totalMP | 352 | 352 | 352 | 352 | 71 |
| totalMaxMP | 395 | 395 | 395 | 395 | 71 |
| totalStaked | 374 | 374 | 374 | 374 | 71 |
| updateAccountMP | 34654 | 36892 | 37156 | 37156 | 19 |
| setTrustedCodehash | 47926 | 47926 | 47926 | 47926 | 35 |
| totalMP | 330 | 330 | 330 | 330 | 71 |
| totalMaxMP | 373 | 373 | 373 | 373 | 71 |
| totalStaked | 352 | 352 | 352 | 352 | 71 |
| updateAccountMP | 34632 | 36870 | 37134 | 37134 | 19 |
| updateGlobalState | 30008 | 55588 | 47387 | 80334 | 25 |


| src/StakeVault.sol:StakeVault contract | | | | | |
|----------------------------------------|-----------------|--------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 857122 | 4070 | | | | |
| 894174 | 4243 | | | | |
| Function Name | min | avg | median | max | # calls |
| stake | 196978 | 234038 | 240671 | 261155 | 46 |
| lock | 36236 | 58390 | 40615 | 98319 | 3 |
| stake | 196956 | 234305 | 240649 | 261178 | 48 |
| unstake | 83287 | 111971 | 101308 | 144002 | 13 |


Expand Down Expand Up @@ -117,9 +118,9 @@
| Deployment Cost | Deployment Size | | | | |
| 639406 | 3369 | | | | |
| Function Name | min | avg | median | max | # calls |
| approve | 46334 | 46343 | 46346 | 46346 | 165 |
| balanceOf | 561 | 1351 | 561 | 2561 | 286 |
| mint | 51284 | 59028 | 51284 | 68384 | 181 |
| approve | 46334 | 46343 | 46346 | 46346 | 180 |
| balanceOf | 561 | 1351 | 561 | 2561 | 291 |
| mint | 51284 | 58959 | 51284 | 68384 | 196 |
| transfer | 34390 | 48070 | 51490 | 51490 | 10 |


Expand Down
67 changes: 35 additions & 32 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
IntegrationTest:testStakeFoo() (gas: 1471979)
IntegrationTest:testStakeFoo() (gas: 1471121)
LockTest:test_LockFailsWithInvalidPeriod() (gas: 285795)
LockTest:test_LockFailsWithNoStake() (gas: 51253)
LockTest:test_LockWithoutPriorLock() (gas: 378889)
NFTMetadataGeneratorSVGTest:testGenerateMetadata() (gas: 92874)
NFTMetadataGeneratorSVGTest:testSetImageStrings() (gas: 60081)
NFTMetadataGeneratorSVGTest:testSetImageStringsRevert() (gas: 35818)
NFTMetadataGeneratorURLTest:testGenerateMetadata() (gas: 109345)
NFTMetadataGeneratorURLTest:testSetBaseURL() (gas: 50653)
NFTMetadataGeneratorURLTest:testSetBaseURLRevert() (gas: 35993)
RewardsStreamerTest:testStake() (gas: 869874)
StakeTest:test_StakeMultipleAccounts() (gas: 488941)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 634452)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 801369)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 494644)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 516001)
StakeTest:test_StakeOneAccount() (gas: 282173)
StakeTest:test_StakeOneAccountAndRewards() (gas: 427680)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 488578)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 483621)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 295784)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 295862)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 295973)
UnstakeTest:test_StakeMultipleAccounts() (gas: 488963)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 634429)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 801391)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 494621)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 516023)
UnstakeTest:test_StakeOneAccount() (gas: 282196)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 427702)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 488600)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 483601)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 295829)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 295862)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 295951)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 500003)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 681122)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 1002814)
UnstakeTest:test_UnstakeOneAccount() (gas: 474888)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 488421)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 579871)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 509781)
StakeTest:test_StakeMultipleAccounts() (gas: 488809)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 634232)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 801063)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 494911)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 516313)
StakeTest:test_StakeOneAccount() (gas: 282063)
StakeTest:test_StakeOneAccountAndRewards() (gas: 427482)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 488293)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 483447)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 295896)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 295974)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 296085)
UnstakeTest:test_StakeMultipleAccounts() (gas: 488831)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 634209)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 801085)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 494888)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 516335)
UnstakeTest:test_StakeOneAccount() (gas: 282086)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 427504)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 488315)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 483427)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 295941)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 295974)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 296063)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 500049)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 680902)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 1002330)
UnstakeTest:test_UnstakeOneAccount() (gas: 474602)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 488113)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 579585)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 510049)
XPNFTTokenTest:testApproveNotAllowed() (gas: 10507)
XPNFTTokenTest:testGetApproved() (gas: 10531)
XPNFTTokenTest:testIsApprovedForAll() (gas: 10705)
Expand Down
42 changes: 40 additions & 2 deletions src/RewardsStreamerMP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
error StakingManager__InvalidLockingPeriod();
error StakingManager__CannotRestakeWithLockedFunds();
error StakingManager__TokensAreLocked();
error StakingManager__AlreadyLocked();

IERC20 public immutable STAKING_TOKEN;
IERC20 public immutable REWARD_TOKEN;
Expand Down Expand Up @@ -79,8 +80,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
uint256 bonusMP = 0;

if (lockPeriod != 0) {
uint256 lockMultiplier = (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR) / MAX_LOCKUP_PERIOD;
bonusMP = amount * lockMultiplier / SCALE_FACTOR;
bonusMP = _calculateBonusMP(amount, lockPeriod);
account.lockUntil = block.timestamp + lockPeriod;
} else {
account.lockUntil = 0;
Expand All @@ -99,6 +99,39 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
account.lastMPUpdateTime = block.timestamp;
}

function lock(uint256 lockPeriod) external onlyTrustedCodehash 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();
}

_updateGlobalState();
_updateAccountMP(msg.sender);

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

// Update account state
account.lockUntil = block.timestamp + lockPeriod;
account.accountMP += additionalBonusMP;
account.maxMP += additionalBonusMP;

// Update global state
totalMP += additionalBonusMP;
totalMaxMP += additionalBonusMP;

account.accountRewardIndex = rewardIndex;
account.lastMPUpdateTime = block.timestamp;
}

function unstake(uint256 amount) external onlyTrustedCodehash nonReentrant {
Account storage account = accounts[msg.sender];
if (amount > account.stakedBalance) {
Expand Down Expand Up @@ -185,6 +218,11 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGu
}
}

function _calculateBonusMP(uint256 amount, uint256 lockPeriod) internal view returns (uint256) {
uint256 lockMultiplier = (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR) / MAX_LOCKUP_PERIOD;
return amount * lockMultiplier / SCALE_FACTOR;
}

function _updateAccountMP(address accountAddress) internal {
Account storage account = accounts[accountAddress];

Expand Down
8 changes: 8 additions & 0 deletions src/StakeVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ contract StakeVault is Ownable {
_stake(_amount, _seconds, _from);
}

/**
* @notice Lock the staked amount for a specified time.
* @param _seconds The time period to lock the staked amount for.
*/
function lock(uint256 _seconds) external onlyOwner {
stakeManager.lock(_seconds);
}

/**
* @notice Unstake a specified amount of tokens and send to the owner.
* @param _amount The amount of tokens to unstake.
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IStakeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface IStakeManager is ITrustedCodehashAccess {
error StakeManager__StakeIsTooLow();

function stake(uint256 _amount, uint256 _seconds) external;
function lock(uint256 _seconds) external;
function unstake(uint256 _amount) external;

function totalStaked() external view returns (uint256);
Expand Down
66 changes: 65 additions & 1 deletion test/RewardsStreamerMP.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ contract StakeTest is RewardsStreamerMPTest {
}

contract UnstakeTest is StakeTest {
function setUp() public override {
function setUp() public virtual override {
super.setUp();
}

Expand Down Expand Up @@ -1449,3 +1449,67 @@ contract UnstakeTest is StakeTest {
);
}
}

contract LockTest is RewardsStreamerMPTest {
function setUp() public virtual override {
super.setUp();
}

function _lock(address account, uint256 lockPeriod) internal {
StakeVault vault = StakeVault(vaults[account]);
vm.prank(account);
vault.lock(lockPeriod);
}

function test_LockWithoutPriorLock() public {
// Setup - alice stakes 10 tokens without lock
uint256 stakeAmount = 10e18;
_stake(alice, stakeAmount, 0);

uint256 initialAccountMP = stakeAmount; // 10e18
uint256 initialMaxMP = stakeAmount * streamer.MAX_MULTIPLIER() + stakeAmount; // 50e18

// Verify initial state
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: stakeAmount,
rewardIndex: 0,
accountMP: initialAccountMP,
maxMP: initialMaxMP
})
);

// Lock for 1 year
uint256 lockPeriod = 365 days;
uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockPeriod);

_lock(alice, lockPeriod);

// Check updated state
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: stakeAmount,
rewardIndex: 0,
accountMP: initialAccountMP + expectedBonusMP,
maxMP: initialMaxMP + expectedBonusMP
})
);
}

function test_LockFailsWithNoStake() public {
vm.expectRevert(RewardsStreamerMP.StakingManager__InsufficientBalance.selector);
_lock(alice, 365 days);
}

function test_LockFailsWithInvalidPeriod() public {
_stake(alice, 10e18, 0);

// Test with period = 0
vm.expectRevert(RewardsStreamerMP.StakingManager__InvalidLockingPeriod.selector);
_lock(alice, 0);
}
}

0 comments on commit 5bc7ebf

Please sign in to comment.