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[][]();
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];
}
}
}
if (totalRewards != 0) {
totalStaked = uint256(int256(totalStaked) + totalRewards);
}
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];
}
}
if (totalFeeAmounts >= totalStaked) {
totalFeeAmounts = 0;
}
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