Liquid Staking

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

StakingPool rewards can get sandwich attacked with flash loans and get stolen from honest stakers

Summary

StakingPool.sol rewards can be sandwich attacked with a flash loan. Exploiters can frontrun the StakingPool::updateStrategyRewards function which transfers tokens in and deposit a huge amount of assets before this. Then, they can backrun it and withdraw the profit from the rewards that just came in and even pay off the flash loan.

Vulnerability Details

The StakingPool::updateStrategyRewards function brings in the new rewards in the StakingPool and increases the totalStaked variable:

StakingPool.sol
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) {
.
.
.
}
OperatorVCS.sol
function updateDeposits(bytes calldata _data)
external
override
onlyStakingPool
returns (int256 depositChange, address[] memory receivers, uint256[] memory amounts)
{
.
.
.
uint256 balance = token.balanceOf(address(this));
.
.
.
if (balance != 0) {
@> token.safeTransfer(address(stakingPool), balance);
newTotalDeposits -= balance;
}
totalDeposits = newTotalDeposits;
totalPrincipalDeposits = newTotalPrincipalDeposits;
}

The exploiter can deposit before rewards update, get some shares which will be worth more assets after the rewards come in and totalStaked gets increased.

function getSharesByStake(uint256 _amount) public view returns (uint256) {
uint256 totalStaked = _totalStaked();
if (totalStaked == 0) {
return _amount;
} else {
return (_amount * totalShares) / totalStaked;
}
}
function getStakeByShares(uint256 _amount) public view returns (uint256) {
if (totalShares == 0) {
return _amount;
} else {
return (_amount * _totalStaked()) / totalShares;
}
}
  1. Let's say there are currently 10e18 totalShares and 20e18 totalStaked

  2. Exploiter frontruns the updateStrategyRewards function and deposits 100e18 assets

  3. According to getSharesByStake, exploiter receives 100e18 * 10e18 / 20e18 = 50e18 shares

  4. updateStrategyRewards increases the totalStaked by 5e18

  5. StakingPool now has 20e18 + 100e18 + 5e18 = 125e18 totalStaked and 10e18 + 50e18 = 60e18 totalShares

  6. Exploiter's 50e18 shares are now worth 50e18 * 125e18 / 60e18 = 104166666666666666666 = 104.16e18 assets

  7. Exploiter can now backrun and withdraw his 1 transaction profit

Impact

Rewards distribution mechanism can be easily exploited and is flashloan-able. Normal stakers will not get the rewards they deserve and exploiters can get a huge portion of them in 1 transaction.

Tools Used

Manual review

Recommendations

Make the deposit a 2-transaction process. Queue every deposit and require users to call another function in a different block to finalize it.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Appeal created

kalogerone Auditor
11 months ago
inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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