Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: medium
Invalid

StakingPool::updateStrategyRewards can run of gas due to unbounded loop in OperatorVCS::updateDeposits

Summary

The updateStrategyRewards function calls the _updateStrategyRewards function, which calls updateDeposits. The updateDeposit function loops based on the vaultCount in OperatorVCS contract. The vaultcount is unbounded, and when the number of vaults increases to a state, when this function is called it will run out of gas.

Vulnerability Details

The updateDeposits function uses the total number of vaults in the updateDeposits function; this contract does not have a maximum number of vaults that can be added, which means that when the number of vault increase to a point, the function will run out of gas leading to Denial of Service.

function updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) external {
if (msg.sender != rebaseController && !_strategyExists(msg.sender)) {
revert SenderNotAuthorized();
}
@> _updateStrategyRewards(_strategyIdxs, _data);
}
function _updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) private {
int256 totalRewards;
uint256 totalFeeAmounts;
uint256 totalFeeCount;
address[][] memory receivers = new address[][]();
uint256[][] memory feeAmounts = new uint256[][]();
// sum up rewards and fees across strategies
for (uint256 i = 0; i < _strategyIdxs.length; ++i) {
IStrategy strategy = IStrategy(strategies[_strategyIdxs[i]]);
(int256 depositChange, address[] memory strategyReceivers, uint256[] memory strategyFeeAmounts) =
@> strategy.updateDeposits(_data);
totalRewards += depositChange;
if (strategyReceivers.length != 0) {
receivers[i] = strategyReceivers;
feeAmounts[i] = strategyFeeAmounts;
totalFeeCount += receivers[i].length;
for (uint256 j = 0; j < strategyReceivers.length; ++j) {
totalFeeAmounts += strategyFeeAmounts[j];
}
}
}
// update totalStaked if there was a net change in deposits
if (totalRewards != 0) {
totalStaked = uint256(int256(totalStaked) + totalRewards);
}
// calulate fees if net positive rewards were earned
if (totalRewards > 0) {
receivers[receivers.length - 1] = new address[]();
feeAmounts[feeAmounts.length - 1] = new uint256[]();
totalFeeCount += fees.length;
for (uint256 i = 0; i < fees.length; i++) {
receivers[receivers.length - 1][i] = fees[i].receiver;
feeAmounts[feeAmounts.length - 1][i] = (uint256(totalRewards) * fees[i].basisPoints) / 10000;
totalFeeAmounts += feeAmounts[feeAmounts.length - 1][i];
}
}
// safety check
if (totalFeeAmounts >= totalStaked) {
totalFeeAmounts = 0;
}
// distribute fees to receivers if there are any
if (totalFeeAmounts > 0) {
uint256 sharesToMint = (totalFeeAmounts * totalShares) / (totalStaked - totalFeeAmounts);
_mintShares(address(this), sharesToMint);
uint256 feesPaidCount;
for (uint256 i = 0; i < receivers.length; i++) {
for (uint256 j = 0; j < receivers[i].length; j++) {
if (feesPaidCount == totalFeeCount - 1) {
transferAndCallFrom(address(this), receivers[i][j], balanceOf(address(this)), "0x");
} else {
transferAndCallFrom(address(this), receivers[i][j], feeAmounts[i][j], "0x");
feesPaidCount++;
}
}
}
}
emit UpdateStrategyRewards(msg.sender, totalStaked, totalRewards, totalFeeAmounts);
}
function updateDeposits(bytes calldata _data)
external
override
onlyStakingPool
returns (int256 depositChange, address[] memory receivers, uint256[] memory amounts)
{
uint256 minRewards = _data.length == 0 ? 0 : abi.decode(_data, (uint256));
uint256 newTotalDeposits = totalDeposits;
uint256 newTotalPrincipalDeposits;
uint256 vaultDeposits;
uint256 operatorRewards;
uint256 vaultCount = vaults.length;
address receiver = address(this);
@> for (uint256 i = 0; i < vaultCount; ++i) {
(uint256 deposits, uint256 principal, uint256 rewards) =
IOperatorVault(address(vaults[i])).updateDeposits(minRewards, receiver);
vaultDeposits += deposits;
newTotalPrincipalDeposits += principal;
operatorRewards += rewards;
}
uint256 balance = token.balanceOf(address(this));
depositChange = int256(vaultDeposits + balance) - int256(totalDeposits);
if (operatorRewards != 0) {
receivers = new address[](1 + (depositChange > 0 ? fees.length : 0));
amounts = new uint256[](receivers.length);
receivers[0] = address(this);
amounts[0] = operatorRewards;
unclaimedOperatorRewards += operatorRewards;
}
if (depositChange > 0) {
newTotalDeposits += uint256(depositChange);
if (receivers.length == 0) {
receivers = new address[](fees.length);
amounts = new uint256[](receivers.length);
for (uint256 i = 0; i < receivers.length; ++i) {
receivers[i] = fees[i].receiver;
amounts[i] = (uint256(depositChange) * fees[i].basisPoints) / 10000;
}
} else {
for (uint256 i = 1; i < receivers.length; ++i) {
receivers[i] = fees[i - 1].receiver;
amounts[i] = (uint256(depositChange) * fees[i - 1].basisPoints) / 10000;
}
}
} else if (depositChange < 0) {
newTotalDeposits -= uint256(depositChange * -1);
}
if (balance != 0) {
token.safeTransfer(address(stakingPool), balance);
newTotalDeposits -= balance;
}
totalDeposits = newTotalDeposits;
totalPrincipalDeposits = newTotalPrincipalDeposits;
}

Impact

Denial of service on the StakingPool::updateStrategyRewards function when the number of vaults in the OperatorVCS contract becomes too large.

Tools Used

Manual Review

Recommendations

Cap the number of vaults that can be created in the OperatorVCS contract

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.