Liquid Staking

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

Vault fee receivers can conditionally block rewards distribution flow

Summary

Rewards distribution flow can be blocked by fee receivers through token transfer callback

Vulnerability Details

The internal function StakingPool#_updateStrategyRewards() is expected to update and distribute rewards if net positive earned from strategies. Fees/rewards are minted as shares and sent to receivers using function ERC677#transferAndCallFrom(), which has contract callback if receiver is a contract.

function _updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) private {
...
...
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);
}

The function StakingPool#_updateStrategyRewards() is called from public functions StakingPool#updateStrategyRewards() and StakingPool#removeStrategy()

function removeStrategy(
uint256 _index,
bytes memory _strategyUpdateData,
bytes calldata _strategyWithdrawalData
) external onlyOwner {
require(_index < strategies.length, "Strategy does not exist");
uint256[] memory idxs = new uint256[]();
idxs[0] = _index;
@> _updateStrategyRewards(idxs, _strategyUpdateData);
IStrategy strategy = IStrategy(strategies[_index]);
uint256 totalStrategyDeposits = strategy.getTotalDeposits(); // @info the new total deposit after changed in updateDeposits()
if (totalStrategyDeposits > 0) {
strategy.withdraw(totalStrategyDeposits, _strategyWithdrawalData); // @question can withdraw all ?
}
for (uint256 i = _index; i < strategies.length - 1; i++) {
strategies[i] = strategies[i + 1]; // @info shifting ==> strategies order does not change
}
strategies.pop();
token.safeApprove(address(strategy), 0);
}
function updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) external {
if (msg.sender != rebaseController && !_strategyExists(msg.sender))
revert SenderNotAuthorized();
@> _updateStrategyRewards(_strategyIdxs, _data);
}

The function StakingPool#updateStrategyRewards() can be called from rebaseController or, called from strategy contracts. In strategy contracts, there are 2 flows that call the function: addFee and updateFee:

function addFee(address _receiver, uint256 _feeBasisPoints) external onlyOwner {
@> _updateStrategyRewards();
fees.push(Fee(_receiver, _feeBasisPoints));
if (_totalFeesBasisPoints() > 3000) revert FeesTooLarge();
}
function updateFee(
uint256 _index,
address _receiver,
uint256 _feeBasisPoints
) external onlyOwner {
@> _updateStrategyRewards();
if (_feeBasisPoints == 0) {
fees[_index] = fees[fees.length - 1];
fees.pop();
} else {
fees[_index].receiver = _receiver;
fees[_index].basisPoints = _feeBasisPoints;
}
if (_totalFeesBasisPoints() > 3000) revert FeesTooLarge();
}
function _updateStrategyRewards() internal {
address[] memory strategies = stakingPool.getStrategies();
uint256[] memory strategyIdxs = new uint256[]();
for (uint256 i = 0; i < strategies.length; ++i) {
strategyIdxs[i] = i;
}
@> stakingPool.updateStrategyRewards(strategyIdxs, "");
}

So, the with the single failure point here at the function _updateStrategyRewards, malicious fee receivers can conditionally block 3 flows: removing strategies, adding fees in strategies, updating fees in strategies

Impact

A malicious fee receiver can conditionally block 3 flows: removing strategies, adding fees in strategies, updating fees in strategies

Tools Used

Manual

Recommendations

Consider adding mechanism to try/catch to skip revert callbacks, or force transfers

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Inability to independently update Fee receivers without sending fees to old receivers

Appeal created

galturok Judge
10 months ago
inallhonesty Lead Judge
10 months ago
demorextess Judge
10 months ago
angrymustacheman Auditor
10 months ago
dimah7 Judge
10 months ago
angrymustacheman Auditor
10 months ago
dimah7 Judge
10 months ago
angrymustacheman Auditor
10 months ago
bigsam Auditor
10 months ago
inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Inability to independently update Fee receivers without sending fees to old receivers

Support

FAQs

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