From 75bf214c6e50d96906e4f8eb47e35b6b0558af38 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Thu, 3 Oct 2024 18:55:12 +0700 Subject: [PATCH 1/5] NAY4-1 Risk of Denial of Service if Rewards Are Not Collected for Too Many Intervals (#146) * feat: add collectRewards exposing interval * refactor: rename to collectRewardsToInterval, update lock functions * chore: add natspec for collectRewardsToInterval --- src/facets/StakingFacet.sol | 11 +++++++++++ src/libs/LibAdmin.sol | 8 ++++++-- test/T02Admin.t.sol | 7 ++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/facets/StakingFacet.sol b/src/facets/StakingFacet.sol index 1f5a815c..3e0d4d0d 100644 --- a/src/facets/StakingFacet.sol +++ b/src/facets/StakingFacet.sol @@ -70,6 +70,17 @@ contract StakingFacet is Modifiers { LibTokenizedVaultStaking._collectRewards(parentId, _entityId, lastPaid); } + /** + * @notice Collect rewards for a staker + * @param _entityId staking entity ID + * @param _interval interval to collect rewards up to + */ + function collectRewardsToInterval(bytes32 _entityId, uint64 _interval) external notLocked { + bytes32 parentId = LibObject._getParent(msg.sender._getIdForAddress()); + + LibTokenizedVaultStaking._collectRewards(parentId, _entityId, _interval); + } + function payReward(bytes32 _stakingRewardId, bytes32 _entityId, bytes32 _rewardTokenId, uint256 _amount) external notLocked assertPrivilege(_entityId, LC.GROUP_ENTITY_ADMINS) { LibTokenizedVaultStaking._payReward(_stakingRewardId, _entityId, _rewardTokenId, _amount); } diff --git a/src/libs/LibAdmin.sol b/src/libs/LibAdmin.sol index 8eb73905..74c55fb5 100644 --- a/src/libs/LibAdmin.sol +++ b/src/libs/LibAdmin.sol @@ -141,12 +141,13 @@ library LibAdmin { s.locked[IDiamondProxy.stake.selector] = true; s.locked[IDiamondProxy.unstake.selector] = true; s.locked[IDiamondProxy.collectRewards.selector] = true; + s.locked[IDiamondProxy.collectRewardsToInterval.selector] = true; s.locked[IDiamondProxy.payReward.selector] = true; s.locked[IDiamondProxy.cancelSimplePolicy.selector] = true; s.locked[IDiamondProxy.createSimplePolicy.selector] = true; s.locked[IDiamondProxy.createEntity.selector] = true; - bytes4[] memory lockedFunctions = new bytes4[](21); + bytes4[] memory lockedFunctions = new bytes4[](22); lockedFunctions[0] = IDiamondProxy.startTokenSale.selector; lockedFunctions[1] = IDiamondProxy.paySimpleClaim.selector; lockedFunctions[2] = IDiamondProxy.paySimplePremium.selector; @@ -168,6 +169,7 @@ library LibAdmin { lockedFunctions[18] = IDiamondProxy.cancelSimplePolicy.selector; lockedFunctions[19] = IDiamondProxy.createSimplePolicy.selector; lockedFunctions[20] = IDiamondProxy.createEntity.selector; + lockedFunctions[21] = IDiamondProxy.collectRewardsToInterval.selector; emit FunctionsLocked(lockedFunctions); } @@ -195,8 +197,9 @@ library LibAdmin { s.locked[IDiamondProxy.cancelSimplePolicy.selector] = false; s.locked[IDiamondProxy.createSimplePolicy.selector] = false; s.locked[IDiamondProxy.createEntity.selector] = false; + s.locked[IDiamondProxy.collectRewardsToInterval.selector] = false; - bytes4[] memory lockedFunctions = new bytes4[](21); + bytes4[] memory lockedFunctions = new bytes4[](22); lockedFunctions[0] = IDiamondProxy.startTokenSale.selector; lockedFunctions[1] = IDiamondProxy.paySimpleClaim.selector; lockedFunctions[2] = IDiamondProxy.paySimplePremium.selector; @@ -218,6 +221,7 @@ library LibAdmin { lockedFunctions[18] = IDiamondProxy.cancelSimplePolicy.selector; lockedFunctions[19] = IDiamondProxy.createSimplePolicy.selector; lockedFunctions[20] = IDiamondProxy.createEntity.selector; + lockedFunctions[21] = IDiamondProxy.collectRewardsToInterval.selector; emit FunctionsUnlocked(lockedFunctions); } diff --git a/test/T02Admin.t.sol b/test/T02Admin.t.sol index 1a224d2c..1d0c969e 100644 --- a/test/T02Admin.t.sol +++ b/test/T02Admin.t.sol @@ -241,7 +241,7 @@ contract T02AdminTest is D03ProtocolDefaults, MockAccounts { assertEq(entries[0].topics[0], keccak256("FunctionsLocked(bytes4[])")); (s_functionSelectors) = abi.decode(entries[0].data, (bytes4[])); - bytes4[] memory lockedFunctions = new bytes4[](21); + bytes4[] memory lockedFunctions = new bytes4[](22); lockedFunctions[0] = IDiamondProxy.startTokenSale.selector; lockedFunctions[1] = IDiamondProxy.paySimpleClaim.selector; lockedFunctions[2] = IDiamondProxy.paySimplePremium.selector; @@ -263,6 +263,7 @@ contract T02AdminTest is D03ProtocolDefaults, MockAccounts { lockedFunctions[18] = IDiamondProxy.cancelSimplePolicy.selector; lockedFunctions[19] = IDiamondProxy.createSimplePolicy.selector; lockedFunctions[20] = IDiamondProxy.createEntity.selector; + lockedFunctions[21] = IDiamondProxy.collectRewardsToInterval.selector; for (uint256 i = 0; i < lockedFunctions.length; i++) { assertTrue(nayms.isFunctionLocked(lockedFunctions[i])); @@ -322,6 +323,9 @@ contract T02AdminTest is D03ProtocolDefaults, MockAccounts { vm.expectRevert("function is locked"); nayms.collectRewards(bytes32(0)); + vm.expectRevert("function is locked"); + nayms.collectRewardsToInterval(bytes32(0), 5); + vm.expectRevert("function is locked"); nayms.cancelSimplePolicy(bytes32(0)); @@ -354,6 +358,7 @@ contract T02AdminTest is D03ProtocolDefaults, MockAccounts { assertFalse(nayms.isFunctionLocked(IDiamondProxy.unstake.selector), "function unstake locked"); assertFalse(nayms.isFunctionLocked(IDiamondProxy.payReward.selector), "function payReward locked"); assertFalse(nayms.isFunctionLocked(IDiamondProxy.collectRewards.selector), "function collectRewards locked"); + assertFalse(nayms.isFunctionLocked(IDiamondProxy.collectRewardsToInterval.selector), "function collectRewardsToInterval locked"); assertFalse(nayms.isFunctionLocked(IDiamondProxy.cancelSimplePolicy.selector), "function cancelSimplePolicy locked"); assertFalse(nayms.isFunctionLocked(IDiamondProxy.createSimplePolicy.selector), "function createSimplePolicy locked"); assertFalse(nayms.isFunctionLocked(IDiamondProxy.createEntity.selector), "function createEntity locked"); From 75b93043f393f178dd602b68e403aac682da3a34 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Thu, 3 Oct 2024 18:55:34 +0700 Subject: [PATCH 2/5] NAY4-8 Unclear Expected Logic at StakingConfig.initDate (#143) --- src/libs/LibTokenizedVaultStaking.sol | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/libs/LibTokenizedVaultStaking.sol b/src/libs/LibTokenizedVaultStaking.sol index a2bb598b..98ec67d6 100644 --- a/src/libs/LibTokenizedVaultStaking.sol +++ b/src/libs/LibTokenizedVaultStaking.sol @@ -49,9 +49,16 @@ library LibTokenizedVaultStaking { emit TokenStakingStarted(_entityId, _config.tokenId, _config.initDate, _config.a, _config.r, _config.divider, _config.interval); } + /** + * @notice Checks if staking has been initialized for the given entity. + * @dev Staking is considered initialized if the initDate is set and the current timestamp is + * equal to or after the initDate. + * @param _entityId The ID of the entity to check staking initialization. + * @return bool indicating whether staking is initialized. + */ function _isStakingInitialized(bytes32 _entityId) internal view returns (bool) { AppStorage storage s = LibAppStorage.diamondStorage(); - return (s.stakingConfigs[_entityId].initDate > 0 && s.stakingConfigs[_entityId].initDate < block.timestamp); + return (s.stakingConfigs[_entityId].initDate > 0 && s.stakingConfigs[_entityId].initDate <= block.timestamp); } function _stakingConfig(bytes32 _entityId) internal view returns (StakingConfig memory) { @@ -78,7 +85,14 @@ library LibTokenizedVaultStaking { AppStorage storage s = LibAppStorage.diamondStorage(); return s.stakeCollected[_entityId][_entityId]; } - + /** + * @notice Pays rewards to a staker. + * @dev Rewards can be paid if the current timestamp is equal to or after the staking initDate. + * @param _stakingRewardId The ID for the staking reward. + * @param _entityId The ID of the entity whose rewards are being paid. + * @param _rewardTokenId The ID of the reward token. + * @param _rewardAmount The amount of reward to be paid. + */ function _payReward(bytes32 _stakingRewardId, bytes32 _entityId, bytes32 _rewardTokenId, uint256 _rewardAmount) internal { AppStorage storage s = LibAppStorage.diamondStorage(); From 299ef591c05258620ea980e4be8aa3c94d1a9902 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Thu, 3 Oct 2024 18:55:46 +0700 Subject: [PATCH 3/5] NAY4-9 Unclear Expected Logic for the Exact Min Allowed for Staking / Reward (#144) --- src/libs/LibTokenizedVaultStaking.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/LibTokenizedVaultStaking.sol b/src/libs/LibTokenizedVaultStaking.sol index 98ec67d6..97b38bfc 100644 --- a/src/libs/LibTokenizedVaultStaking.sol +++ b/src/libs/LibTokenizedVaultStaking.sol @@ -124,7 +124,7 @@ library LibTokenizedVaultStaking { s.stakeBalance[vTokenId][_entityId] = stakingState.balance; s.stakeBoost[vTokenId][_entityId] = stakingState.boost; - // Update last colleted interval for the token itself + // Update last collected interval for the token itself s.stakeCollected[_entityId][_entityId] = interval; // Transfer the funds @@ -142,7 +142,8 @@ library LibTokenizedVaultStaking { bytes32 tokenId = s.stakingConfigs[_entityId].tokenId; - if (_amount < s.objectMinimumSell[tokenId]) revert InvalidStakingAmount(); + // Prevent staking below or equal to the minimum required + if (_amount <= s.objectMinimumSell[tokenId]) revert InvalidStakingAmount(); uint64 currentInterval = _currentInterval(_entityId); bytes32 vTokenIdMax = _vTokenIdBucket(_entityId, tokenId); From fafdffed68e1e2fb050b2a650b6d302448d3f2b1 Mon Sep 17 00:00:00 2001 From: Kevin Park Date: Thu, 3 Oct 2024 18:56:11 +0700 Subject: [PATCH 4/5] NAY4-S2 Documentation Improvement (#147) * NAY4-S3-3 update docs * NAY4-S3-4 clarify usage of IERC1271 * NAY4-S3-5 _addUniqueValue follow standard internal functions naming convention * NAY4-S3-6 update natspec for _vTokenId * NAY4-S3-7 clarify logic in rewards_ * NAY4-S3-8 add comments about staking related mapping variables * NAY4-S3-9 fix natspec typo for _calculateStartTimeOfInterval * update staking related mappings descriptions --- docs/src/src/libs/LibAdmin.sol/library.LibAdmin.md | 7 ------- .../src/libs/LibHelpers.sol/library.LibHelpers.md | 14 -------------- .../FreeStructs.sol/struct.StakingCheckpoint.md | 13 ------------- src/interfaces/IERC1271.sol | 1 + src/libs/LibTokenizedVaultStaking.sol | 9 +++++---- src/shared/AppStorage.sol | 14 ++++++++++++++ 6 files changed, 20 insertions(+), 38 deletions(-) delete mode 100644 docs/src/src/shared/FreeStructs.sol/struct.StakingCheckpoint.md diff --git a/docs/src/src/libs/LibAdmin.sol/library.LibAdmin.md b/docs/src/src/libs/LibAdmin.sol/library.LibAdmin.md index 116f54d0..67ec5ed0 100644 --- a/docs/src/src/libs/LibAdmin.sol/library.LibAdmin.md +++ b/docs/src/src/libs/LibAdmin.sol/library.LibAdmin.md @@ -10,13 +10,6 @@ function _getSystemId() internal pure returns (bytes32); ``` -### _getEmptyId - - -```solidity -function _getEmptyId() internal pure returns (bytes32); -``` - ### _updateMaxDividendDenominations diff --git a/docs/src/src/libs/LibHelpers.sol/library.LibHelpers.md b/docs/src/src/libs/LibHelpers.sol/library.LibHelpers.md index d1251d82..7f3770aa 100644 --- a/docs/src/src/libs/LibHelpers.sol/library.LibHelpers.md +++ b/docs/src/src/libs/LibHelpers.sol/library.LibHelpers.md @@ -5,13 +5,6 @@ Pure functions ## Functions -### _getIdForObjectAtIndex - - -```solidity -function _getIdForObjectAtIndex(uint256 _index) internal pure returns (bytes32); -``` - ### _getIdForAddress @@ -33,13 +26,6 @@ function _getSenderId() internal view returns (bytes32); function _checkBottom12BytesAreEmpty(bytes32 value) internal pure returns (bool); ``` -### _checkUpper12BytesAreEmpty - - -```solidity -function _checkUpper12BytesAreEmpty(bytes32 value) internal pure returns (bool); -``` - ### _getAddressFromId diff --git a/docs/src/src/shared/FreeStructs.sol/struct.StakingCheckpoint.md b/docs/src/src/shared/FreeStructs.sol/struct.StakingCheckpoint.md deleted file mode 100644 index a4ff1b1a..00000000 --- a/docs/src/src/shared/FreeStructs.sol/struct.StakingCheckpoint.md +++ /dev/null @@ -1,13 +0,0 @@ -# StakingCheckpoint -[Git Source](https://github.com/nayms/contracts-v3/blob/0aa70a4d39a9875c02cd43cc38c09012f52d800e/src/shared/FreeStructs.sol) - - -```solidity -struct StakingCheckpoint { - int128 bias; - int128 slope; - uint256 ts; - uint256 blk; -} -``` - diff --git a/src/interfaces/IERC1271.sol b/src/interfaces/IERC1271.sol index a56057ba..42cef2b8 100644 --- a/src/interfaces/IERC1271.sol +++ b/src/interfaces/IERC1271.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.20; /** * @dev Interface of the ERC1271 standard signature validation method for * contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271]. + * @dev We use this interface to generate java bindings for solidity contracts. */ interface IERC1271 { /** diff --git a/src/libs/LibTokenizedVaultStaking.sol b/src/libs/LibTokenizedVaultStaking.sol index 97b38bfc..581865b7 100644 --- a/src/libs/LibTokenizedVaultStaking.sol +++ b/src/libs/LibTokenizedVaultStaking.sol @@ -19,6 +19,7 @@ library LibTokenizedVaultStaking { /** * @dev First 4 bytes: "VTOK", next 8 bytes: interval, next 20 bytes: right 20 bytes of tokenId + * @param _entityId The ID of the entity. * @param _tokenId The internal ID of the token. * @param _interval The interval of staking. */ @@ -270,7 +271,7 @@ library LibTokenizedVaultStaking { uint256 totalDistributionAmount = s.stakingDistributionAmount[_vTokenId(_entityId, tokenId, i)]; if (totalDistributionAmount > 0) { uint256 currencyIndex; - (rewards, currencyIndex) = addUniqueValue(rewards, s.stakingDistributionDenomination[_vTokenId(_entityId, tokenId, i)]); + (rewards, currencyIndex) = _addUniqueValue(rewards, s.stakingDistributionDenomination[_vTokenId(_entityId, tokenId, i)]); // Use the same math as dividend distributions, assuming zero has already been collected uint256 userDistributionAmount = LibTokenizedVault._getWithdrawableDividendAndDeductionMath( @@ -362,7 +363,7 @@ library LibTokenizedVaultStaking { return s.stakingConfigs[_entityId].divider; } - function addUniqueValue(RewardsBalances memory rewards, bytes32 newValue) internal pure returns (RewardsBalances memory, uint256) { + function _addUniqueValue(RewardsBalances memory rewards, bytes32 newValue) internal pure returns (RewardsBalances memory, uint256) { require(rewards.currencies.length == rewards.amounts.length, "Different array lengths!"); uint256 length = rewards.currencies.length; @@ -375,7 +376,7 @@ library LibTokenizedVaultStaking { // prettier-ignore RewardsBalances memory rewards_ = RewardsBalances({ currencies: new bytes32[](length + 1), - amounts: new uint256[](rewards.amounts.length + 1), + amounts: new uint256[](length + 1), lastPaidInterval: 0 }); @@ -392,7 +393,7 @@ library LibTokenizedVaultStaking { /** * @dev Get the starting time of a given interval - * @param _entityId The internal ID of the token + * @param _entityId The internal ID of the entity * @param _interval The interval to get the time for */ function _calculateStartTimeOfInterval(bytes32 _entityId, uint64 _interval) internal view returns (uint64 intervalTime_) { diff --git a/src/shared/AppStorage.sol b/src/shared/AppStorage.sol index 7b12846e..c98f9336 100644 --- a/src/shared/AppStorage.sol +++ b/src/shared/AppStorage.sol @@ -91,6 +91,20 @@ struct AppStorage { mapping(bytes32 entityId => mapping(bytes32 stakerId => uint64 interval)) stakingSynced; // last interval when data was synced into storage for staker } +/// Staking-Related Mappings + +/// | Mapping Name | Key Structure | Value Type | Description | +/// |-----------------------------------|----------------------------------------|------------|-------------------------------------------------------------------------------------------------------------| +/// | `stakeCollected` | `[Entity ID][Staker ID]` | `uint64` | Records the last timestamp a staker collected their stake. | +/// | `stakeCollected` | `[Entity ID][Entity ID]` | `uint64` | Records the last timestamp an entity paid out rewards. | +/// | `stakeBalance` | `[vTokenId][Account ID]` | `uint256` | Tracks staked balances for accounts across different intervals. | +/// | `stakeBoost` | `[vTokenId][Account ID]` | `uint256` | Tracks boosted staked balances for accounts. | +/// | `stakeBalanceAdded` | `[vTokenId][Staker ID]` | `uint256` | Raw balance staked at an interval without any boost, used for future interval calculations. | +/// | `stakingDistributionAmount` | `[vTokenId]` | `uint256` | Stores the reward amount for each `vTokenId` at each interval. | +/// | `stakingDistributionDenomination` | `[vTokenId]` | `bytes32` | Stores the reward currency (`denomination`) for each `vTokenId` at each interval. | +/// | `stakingSynced` | `[Entity ID][Staker ID]` | `uint64` | Records the last interval when data was synced into storage for a staker. | +/// | `objectMinimumSell` | `[Token ID][Entity ID]` | `uint256` | Sets minimum staking and reward amounts for tokens per entity. | + struct FunctionLockedStorage { mapping(bytes4 => bool) locked; // function selector => is locked? } From 93fe561c4080e50952523e4fcda5a0829f33021b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksandar=20Marinkovi=C4=87?= <520902+amarinkovic@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:03:50 +0200 Subject: [PATCH 5/5] NAY4-4 Incorrect boosted balance (#141) * chore: reproduce the issue * fix: boosted balance calculation * doc: comment the boosted balance calculation * fix: better boosted balance calculation * fix: boosted balance workaround * chore: remove logging * refactor: revert argument reordering --- src/libs/LibTokenizedVaultStaking.sol | 32 +++++++++++------------- src/shared/AppStorage.sol | 3 ++- test/T06Staking.t.sol | 36 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/libs/LibTokenizedVaultStaking.sol b/src/libs/LibTokenizedVaultStaking.sol index 581865b7..6946e20b 100644 --- a/src/libs/LibTokenizedVaultStaking.sol +++ b/src/libs/LibTokenizedVaultStaking.sol @@ -168,18 +168,25 @@ library LibTokenizedVaultStaking { uint256 boost1 = ((((_getD(_entityId) - ratio) * _amount) / _getD(_entityId)) * _getA(_entityId)) / _getD(_entityId); uint256 boost2 = (((ratio * _amount) / _getD(_entityId)) * _getA(_entityId)) / _getD(_entityId); + uint256 balance1 = _amount - (ratio * _amount) / _getD(_entityId); uint256 balance2 = (ratio * _amount) / _getD(_entityId); s.stakeBalance[_vTokenId(_entityId, tokenId, currentInterval + 1)][_stakerId] += balance1 + boost1; s.stakeBalance[_vTokenId(_entityId, tokenId, currentInterval + 1)][_entityId] += balance1 + boost1; + s.stakeBalanceAdded[_vTokenId(_entityId, tokenId, currentInterval + 1)][_stakerId] += balance1; + s.stakeBalanceAdded[_vTokenId(_entityId, tokenId, currentInterval + 1)][_entityId] += balance1; + s.stakeBoost[_vTokenId(_entityId, tokenId, currentInterval + 1)][_stakerId] += (boost1 * _getR(_entityId)) / _getD(_entityId) + boost2; s.stakeBoost[_vTokenId(_entityId, tokenId, currentInterval + 1)][_entityId] += (boost1 * _getR(_entityId)) / _getD(_entityId) + boost2; s.stakeBalance[_vTokenId(_entityId, tokenId, currentInterval + 2)][_stakerId] += balance2; s.stakeBalance[_vTokenId(_entityId, tokenId, currentInterval + 2)][_entityId] += balance2; + s.stakeBalanceAdded[_vTokenId(_entityId, tokenId, currentInterval + 2)][_stakerId] += balance2; + s.stakeBalanceAdded[_vTokenId(_entityId, tokenId, currentInterval + 2)][_entityId] += balance2; + emit TokenStaked(_stakerId, _entityId, tokenId, _amount); } @@ -405,36 +412,25 @@ library LibTokenizedVaultStaking { intervalTime_ = _calculateStartTimeOfInterval(_entityId, _currentInterval(_entityId)); } - function _stakedAmount(bytes32 _stakerId, bytes32 _entityId) internal view returns (uint256) { + function _getStakingAmounts(bytes32 _stakerId, bytes32 _entityId) internal view returns (uint256 stakedBalance_, uint256 boostedBalance_) { AppStorage storage s = LibAppStorage.diamondStorage(); - bytes32 tokenId = s.stakingConfigs[_entityId].tokenId; - bytes32 vTokenIdMax = _vTokenIdBucket(_entityId, tokenId); - - return s.stakeBalance[vTokenIdMax][_stakerId]; - } - - function _getStakingAmounts(bytes32 _stakerId, bytes32 _entityId) internal view returns (uint256 stakedBalance_, uint256 boostedBalance_) { uint64 currentInterval = _currentInterval(_entityId); + bytes32 tokenId = s.stakingConfigs[_entityId].tokenId; - stakedBalance_ = _stakedAmount(_stakerId, _entityId); + stakedBalance_ = s.stakeBalance[_vTokenIdBucket(_entityId, tokenId)][_stakerId]; if (!_isStakingInitialized(_entityId)) { // boost is always 1 before init return (stakedBalance_, boostedBalance_); } - (StakingState memory state, ) = _getStakingStateWithRewardsBalances(_stakerId, _entityId, currentInterval + 2); - - uint256 boostPrevious = state.boost; - uint256 balancePrevious = state.balance; + (StakingState memory state, ) = _getStakingStateWithRewardsBalances(_stakerId, _entityId, currentInterval); - for (uint i = 0; i < 2; i++) { - boostPrevious = (boostPrevious * _getD(_entityId)) / _getR(_entityId); - balancePrevious = balancePrevious - boostPrevious; - } + uint256 balance1 = s.stakeBalanceAdded[_vTokenId(_entityId, tokenId, currentInterval + 1)][_stakerId]; + uint256 balance2 = s.stakeBalanceAdded[_vTokenId(_entityId, tokenId, currentInterval + 2)][_stakerId]; - boostedBalance_ = balancePrevious; + boostedBalance_ = state.balance + balance1 + balance2; if (boostedBalance_ < stakedBalance_) { boostedBalance_ = stakedBalance_; diff --git a/src/shared/AppStorage.sol b/src/shared/AppStorage.sol index c98f9336..852cb7a8 100644 --- a/src/shared/AppStorage.sol +++ b/src/shared/AppStorage.sol @@ -83,12 +83,13 @@ struct AppStorage { mapping(address userAddress => EntityApproval) selfOnboarding; // map address => { entityId, roleId } /// Staking mapping(bytes32 entityId => StakingConfig) stakingConfigs; // StakingConfig for an entity - mapping(bytes32 vTokenId => mapping(bytes32 stakerId => uint256 balance)) stakeBalance; // [vTokenId][ownerId] boost at interval + mapping(bytes32 vTokenId => mapping(bytes32 stakerId => uint256 balance)) stakeBalance; // [vTokenId][ownerId] balance at interval mapping(bytes32 vTokenId => mapping(bytes32 stakerId => uint256 boost)) stakeBoost; // [vTokenId][ownerId] boost at interval mapping(bytes32 entityId => mapping(bytes32 stakerId => uint64 interval)) stakeCollected; // last interval reward was collected or pain for a staker in staking entity mapping(bytes32 vTokenId => uint256 amount) stakingDistributionAmount; // [vTokenId] Reward at interval mapping(bytes32 vTokenId => bytes32 denomination) stakingDistributionDenomination; // [vTokenId] Reward currency mapping(bytes32 entityId => mapping(bytes32 stakerId => uint64 interval)) stakingSynced; // last interval when data was synced into storage for staker + mapping(bytes32 vTokenId => mapping(bytes32 stakerId => uint256 balance)) stakeBalanceAdded; // raw balance staked at an interval, withouth any boost included, only for reading future intervals (to calculate the total boosted balance) } /// Staking-Related Mappings diff --git a/test/T06Staking.t.sol b/test/T06Staking.t.sol index d386d4cf..21d87a1e 100644 --- a/test/T06Staking.t.sol +++ b/test/T06Staking.t.sol @@ -1325,6 +1325,42 @@ contract T06Staking is D03ProtocolDefaults { // printAppstorage(); } + function test_stake_QS_4_4() public { + uint256 startTime = block.timestamp + 1; + uint256 stake1Time = startTime + 15 days; + uint256 stake2Time = startTime + 31 days; + + initStaking(startTime); + + vm.warp(stake1Time); + + c.log("-- Stake 1 ETH -- ".yellow()); + startPrank(bob); + nayms.stake(nlf.entityId, 1 ether); + + (uint256 stakedBalance, uint256 boostedBalance) = nayms.getStakingAmounts(bob.entityId, nlf.entityId); + assertEq(stakedBalance, boostedBalance, "Bob should have no boost".red()); + printCurrentState(nlf.entityId, bob.entityId, "Bob"); + + vm.warp(stake2Time); + c.log("-- WARP 31 DAYS -- ".yellow()); + printCurrentState(nlf.entityId, bob.entityId, "Bob"); + + uint256 bobBoost = calculateBoost(startTime, stake2Time, R, I, SCALE_FACTOR); + uint256 boostedBalance1 = (0.5 ether * bobBoost) / SCALE_FACTOR / SCALE_FACTOR + 0.5 ether; + + (, uint256 boostedBalance2) = nayms.getStakingAmounts(bob.entityId, nlf.entityId); + assertEq(boostedBalance2, boostedBalance1, "Bob should have boost at[1]".red()); + + c.log("~~~ Stake 1 ETH -- ".yellow()); + nayms.stake(nlf.entityId, 1 ether); + + (, uint256 boostedBalance3) = nayms.getStakingAmounts(bob.entityId, nlf.entityId); + assertEq(boostedBalance3, boostedBalance1 + 1 ether, "Bob should have boost and more stake".red()); + + printCurrentState(nlf.entityId, bob.entityId, "Bob"); + } + function printAppstorage() public { uint64 interval = currentInterval() + 2;