Attacker can exploit lock update logic on secondary chains to increase the amount of rewards sent to a specific secondary chain
Users with existing reSDL NFTs on secondary chains (prior to a decrease in maxBoost) are able to increase `queuedRESDLSupplyChange` by a greater amount than should be possible given the current `maxBoost` value, which then allows them to funnel more rewards to their secondary chain (as `queuedRESDLSupplyChange` maps to `reSDLSupplyByChain[...]`, which is used to calculate the rewards distributed to each secondary chain).\\nConsider the scenario in which the stake.link team is decreasing the `maxBoost` value of the `LinearBoostController` so that newer depositors will get less rewards than OG depositors. This will allow an attacker on a secondary chain to perform the following attack to fraudulently increase the amount of rewards sent to their chain:\\nWe will assume for simplicity that the starting values for the `LinearBoostController` contract is a maxBoost=10 and `maxLockingDuration` = 10_000 seconds. The attacker starts with a single (for simplicity) reSDL NFT on a secondary chain which has amount=100_000 and lockingDuration= 5_000 seconds, meaning their boost is calculated to be: 100_000 * 10 * 5_000/10_000 = 500_000.\\nThen, the stake.link team decreases `maxBoost` to 5. Following this, the attacker will first call `SDLPoolSecondary:extendLockDuration` with a `_lockingDuration` of 9_999, which then calls the internal `_queueLockUpdate`, which is defined as follows:\\n```\\nfunction _queueLockUpdate(\\n    address _owner,\\n    uint256 _lockId,\\n    uint256 _amount,\\n    uint64 _lockingDuration\\n) internal onlyLockOwner(_lockId, _owner) {\\n    Lock memory lock = _getQueuedLockState(_lockId);\\n    LockUpdate memory lockUpdate = LockUpdate(updateBatchIndex, _updateLock(lock, _amount, _lockingDuration));\\n    queuedLockUpdates[_lockId].push(lockUpdate);\\n    queuedRESDLSupplyChange +=\\n        int256(lockUpdate.lock.amount + lockUpdate.lock.boostAmount) -\\n        int256(lock.amount + lock.boostAmount);\\n    // rest of code\\n}\\n```\\n\\nAs part of this function call, `_updateLock` is triggered to perform this update, which is defined as follows:\\n```\\nfunction _updateLock(\\n    Lock memory _lock,\\n    uint256 _amount,\\n    uint64 _lockingDuration\\n) internal view returns (Lock memory) {\\n    if ((_lock.expiry == 0 || _lock.expiry > block.timestamp) && _lockingDuration < _lock.duration) {\\n        revert InvalidLockingDuration();\\n    }\\n\\n    Lock memory lock = Lock(_lock.amount, _lock.boostAmount, _lock.startTime, _lock.duration, _lock.expiry);\\n\\n    uint256 baseAmount = _lock.amount + _amount;\\n    uint256 boostAmount = boostController.getBoostAmount(baseAmount, _lockingDuration);\\n\\n    // rest of code\\n    lock.boostAmount = boostAmount;\\n    // rest of code\\n}\\n```\\n\\nMost important to note here is that (1) since the `_lockingDuration` of 9_999 is greater than the existing duration of 5_000, this call will succeed, and (2) the `boostAmount` is recalculated now using the new `maxBoost` value of 5. We can calculate the new attacker's `boostAmount` to be: 100_000 * 5 * 9_9999/10_000 = 499_950. Since this value is less than the previous 500_000, `queuedRESDLSupplyChange` in the `_queueLockUpdate` call will be decremented by 50.\\nAfter the `SDLPoolSecondary:extendLockDuration` function call is complete, this update will be queued. At some point an update to this secondary SDL pool will be triggered & once that's complete, the attacker will then be able to execute this update. To do so, the attacker calls `executeQueuedOperations`, specifying their reNFT, which then triggers `_executeQueuedLockUpdates` which has the following logic:\\n```\\n// rest of code\\nuint256 numUpdates = queuedLockUpdates[lockId].length;\\n\\nLock memory curLockState = locks[lockId];\\nuint256 j = 0;\\nwhile (j < numUpdates) {\\n if (queuedLockUpdates[lockId][j].updateBatchIndex > finalizedBatchIndex) break;\\n\\n Lock memory updateLockState = queuedLockUpdates[lockId][j].lock;\\n int256 baseAmountDiff = int256(updateLockState.amount) - int256(curLockState.amount);\\n int256 boostAmountDiff = int256(updateLockState.boostAmount) - int256(curLockState.boostAmount);\\n\\n if (baseAmountDiff < 0) {\\n  // rest of code\\n } else if (boostAmountDiff < 0) {\\n  locks[lockId].expiry = updateLockState.expiry;\\n  locks[lockId].boostAmount = 0;\\n  emit InitiateUnlock(_owner, lockId, updateLockState.expiry);\\n } else {\\n  // rest of code\\n }\\n // rest of code\\n}\\n// rest of code\\n```\\n\\nRecall that the attacker only has a single update, with the only difference being the decrease of 50 for the `boostAmount`. This will trigger the logic based on the `boostAmountDiff < 0` statement which will set `locks[lockId].boostAmount = 0`. This is clearly incorrect logic & will allow the attacker to then fraudulently increase `queuedRESDLSupplyChange`, which will ultimately lead to more rewards going to this secondary chain.\\nContinuing this attack, the attacker will again call `SDLPoolSecondary:extendLockDuration`, but this time with a `_lockingDuration` of 10_000. Referencing the same code snippet as earlier, in `_updateLock`, `boostAmount` is now being calculated as: 100_000 * 5 * 10_000/10_000 = 500_000. In `_queueLockUpdate`, `queuedRESDLSupplyChange` is calculated to be: (100_000 + 500_000) - (100_000 + 0) = 500_000, based on this equation:\\n```\\nqueuedRESDLSupplyChange +=\\n int256(lockUpdate.lock.amount + lockUpdate.lock.boostAmount) -\\n int256(lock.amount + lock.boostAmount);\\n```\\n\\nRecall that this value of 0 comes from the improper logic in the `_executeQueuedLockUpdates` function call. Ultimately, in aggregate, `queuedRESDLSupplyChange` has been increased by 500_000 - 50 = 499_950. Had the attacker simply increased their locking duration to the max value of 10_000 after the update, there would be 0 change in the `queuedRESDLSupplyChange`.\\nThe fundamental bug here is that post a decrease in `maxBoost`, the update logic allows all existing reSDL NFTs to be able to increase `queuedRESDLSupplyChange` more than should be possible, & `queuedRESDLSupplyChange` is a major factor in terms of the percentage of rewards going to a given secondary chain.
The `_executeQueuedLockUpdates` function implicitly assumes if there's a decrease in `boostAmountDiff` then the lock update comes from calling `initiateUnlock`. There needs to be an additional case to handle this scenario due to a decrease in the `maxBoost`.
Users with existing reSDL NFTs on secondary chains (prior to a decrease in the maxBoost) are able to increase `queuedRESDLSupplyChange` by a greater amount than should be possible given the current `maxBoost` value, which then allows them to funnel more rewards to their secondary chain.
```\\nfunction _queueLockUpdate(\\n    address _owner,\\n    uint256 _lockId,\\n    uint256 _amount,\\n    uint64 _lockingDuration\\n) internal onlyLockOwner(_lockId, _owner) {\\n    Lock memory lock = _getQueuedLockState(_lockId);\\n    LockUpdate memory lockUpdate = LockUpdate(updateBatchIndex, _updateLock(lock, _amount, _lockingDuration));\\n    queuedLockUpdates[_lockId].push(lockUpdate);\\n    queuedRESDLSupplyChange +=\\n        int256(lockUpdate.lock.amount + lockUpdate.lock.boostAmount) -\\n        int256(lock.amount + lock.boostAmount);\\n    // rest of code\\n}\\n```\\n