Skip to content

Commit

Permalink
feat: collectRewardsThroughInterval, prevent collecting more than 48 …
Browse files Browse the repository at this point in the history
…intervals at once
  • Loading branch information
kevin-fruitful committed Mar 27, 2024
1 parent 2f08d24 commit 22335fa
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 3 deletions.
6 changes: 6 additions & 0 deletions src/facets/StakingFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ contract StakingFacet is Modifiers {
LibTokenizedVaultStaking._collectRewards(parentId, _entityId, interval);
}

function collectRewardsThroughInterval(bytes32 _entityId, uint64 _interval) external notLocked(msg.sig) {
bytes32 parentId = LibObject._getParent(msg.sender._getIdForAddress());

LibTokenizedVaultStaking._collectRewards(parentId, _entityId, _interval);
}

function getStakingState(bytes32 _stakerId, bytes32 _entityId) external view returns (StakingState memory) {
return LibTokenizedVaultStaking._getStakingState(_stakerId, _entityId);
}
Expand Down
7 changes: 7 additions & 0 deletions src/libs/LibConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ library LibConstants {
uint256 internal constant STAKING_MAXTIME = 4 * 365 days; // 4 years max lock
uint256 internal constant SCALE = 1e18; //10 ^ 18

/*///////////////////////////////////////////////////////////////////////////
Staking Constants
///////////////////////////////////////////////////////////////////////////*/

/// @dev The maximum number of intervals that can be collected at once.
uint8 internal constant MAX_COLLECTABLE_INTERVALS = 48;

/// _depositFor Types for events
int128 internal constant STAKING_DEPOSIT_FOR_TYPE = 0;
int128 internal constant STAKING_CREATE_LOCK_TYPE = 1;
Expand Down
8 changes: 6 additions & 2 deletions src/libs/LibTokenizedVaultStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { LibObject } from "./LibObject.sol";
import { LibTokenizedVault } from "../libs/LibTokenizedVault.sol";
import { StakingConfig, StakingState, RewardsBalances } from "../shared/FreeStructs.sol";

import { StakingNotStarted, StakingAlreadyStarted, IntervalRewardPayedOutAlready, InvalidAValue, InvalidRValue, InvalidDividerValue, InvalidStakingInitDate, APlusRCannotBeGreaterThanDivider, InvalidIntervalSecondsValue, InvalidTokenRewardAmount } from "../shared/CustomErrors.sol";
import { StakingNotStarted, StakingAlreadyStarted, IntervalRewardPayedOutAlready, InvalidAValue, InvalidRValue, InvalidDividerValue, InvalidStakingInitDate, APlusRCannotBeGreaterThanDivider, InvalidIntervalSecondsValue, InvalidTokenRewardAmount, ExceededMaxCollectableIntervals } from "../shared/CustomErrors.sol";

library LibTokenizedVaultStaking {
event TokenStakingStarted(bytes32 indexed entityId, bytes32 tokenId, uint256 initDate, uint64 a, uint64 r, uint64 divider, uint64 interval);
Expand Down Expand Up @@ -115,7 +115,8 @@ library LibTokenizedVaultStaking {
bytes32 nextVTokenId = _vTokenId(tokenId, currentInterval + 1);

// First collect rewards. This will update the current state.
_collectRewards(_stakerId, _entityId, currentInterval);
// If the user does not have anything staked, then skip `_collectRewards()`.
if (s.stakeBalance[vTokenIdMax][_stakerId] != 0) _collectRewards(_stakerId, _entityId, currentInterval);

// get the tokens
LibTokenizedVault._internalTransfer(_stakerId, vTokenIdMax, tokenId, _amount);
Expand Down Expand Up @@ -205,6 +206,9 @@ library LibTokenizedVaultStaking {

state.balance = s.stakeBalance[_vTokenId(tokenId, state.lastCollectedInterval)][_stakerId];
state.boost = s.stakeBoost[_vTokenId(tokenId, state.lastCollectedInterval)][_stakerId];

if (_interval - state.lastCollectedInterval > LC.MAX_COLLECTABLE_INTERVALS)
revert ExceededMaxCollectableIntervals(_interval - state.lastCollectedInterval, LC.MAX_COLLECTABLE_INTERVALS);
for (uint64 i = state.lastCollectedInterval; i < _interval; ++i) {
// check to see if there are rewards for this interval, and update arrays
totalDistributionAmount = s.stakingDistributionAmount[_vTokenId(tokenId, i)];
Expand Down
3 changes: 3 additions & 0 deletions src/shared/CustomErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,6 @@ error InvalidTokenRewardAmount(bytes32 guid, bytes32 entityId, bytes32 rewardTok

/// @dev Insuficient balance available to perform the transfer of funds
error InsufficientBalance(bytes32 tokenId, bytes32 from, uint256 balance, uint256 amount);

/// @dev Exceeded the manximum number of collectable intervals. Collect a smaller number of intervals with the method `collectRewardsThroughInterval`.
error ExceededMaxCollectableIntervals(uint256 intervals, uint256 maxIntervals);
51 changes: 50 additions & 1 deletion test/T06Staking.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { DummyToken } from "./utils/DummyToken.sol";

import { LibTokenizedVaultStaking } from "src/libs/LibTokenizedVaultStaking.sol";

import { IntervalRewardPayedOutAlready, InvalidTokenRewardAmount } from "src/shared/CustomErrors.sol";
import { IntervalRewardPayedOutAlready, InvalidTokenRewardAmount, ExceededMaxCollectableIntervals } from "src/shared/CustomErrors.sol";

function makeId2(bytes12 _objecType, bytes20 randomBytes) pure returns (bytes32) {
return bytes32((_objecType)) | (bytes32(randomBytes));
Expand Down Expand Up @@ -698,6 +698,55 @@ contract T06Staking is D03ProtocolDefaults {
assertEq(nayms.internalBalanceOf(bob.entityId, wethId), 1 ether);
}

function test_collectRewardsThroughInterval() public {
initStaking({ initDate: 1 });
c.log(" ~ [%s] Staking start".blue(), nayms.currentInterval(nlf.entityId));

vm.warp(31 days);

startPrank(bob);
nayms.stake(nlf.entityId, bobStakeAmount);

vm.warp(61 days);

assertEq(nayms.lastIntervalPaid(nlf.entityId), 0, "Last interval paid should be 0");

startPrank(nlf);
nayms.payReward(makeId(LC.OBJECT_TYPE_STAKING_REWARD, bytes20("1")), nlf.entityId, usdcId, rewardAmount); // 100 USDC
c.log(" ~ [%s] Reward paid out".blue(), nayms.currentInterval(nlf.entityId));

vm.warp(91 days);

nayms.payReward(makeId(LC.OBJECT_TYPE_STAKING_REWARD, bytes20("2")), nlf.entityId, wethId, 1 ether);
c.log(" ~ [%s] Reward paid out".blue(), nayms.currentInterval(nlf.entityId));

vm.warp(151 days);

startPrank(bob);
nayms.internalBalanceOf(bob.entityId, usdcId);
// Collect only the 1st reward
nayms.collectRewardsThroughInterval(nlf.entityId, 3);
assertEq(nayms.internalBalanceOf(bob.entityId, usdcId), rewardAmount);
assertEq(nayms.internalBalanceOf(bob.entityId, wethId), 0);

// Collect only the 2nd reward
nayms.collectRewardsThroughInterval(nlf.entityId, 4);
assertEq(nayms.internalBalanceOf(bob.entityId, usdcId), rewardAmount);
assertEq(nayms.internalBalanceOf(bob.entityId, wethId), 1 ether);
}

function test_MaxCollectRewardsThroughInterval() public {
initStaking({ initDate: 1 });
c.log(" ~ [%s] Staking start".blue(), nayms.currentInterval(nlf.entityId));

vm.warp(1801 days);
startPrank(bob);
nayms.stake(nlf.entityId, bobStakeAmount);

vm.expectRevert(abi.encodeWithSelector(ExceededMaxCollectableIntervals.selector, 60, LC.MAX_COLLECTABLE_INTERVALS));
nayms.collectRewardsThroughInterval(nlf.entityId, 60);
}

function calculateBalanceAtTime(uint256 t, uint256 initialBalance) public pure returns (uint256 boostedBalanceAtTime) {
uint256 Xm = calculateMultiplier(t, I, A, R);
boostedBalanceAtTime = (initialBalance * Xm) / SCALE_FACTOR;
Expand Down

0 comments on commit 22335fa

Please sign in to comment.