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[][]();
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) {
.
.
.
}
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;
}
}
Let's say there are currently 10e18 totalShares
and 20e18 totalStaked
Exploiter frontruns the updateStrategyRewards
function and deposits 100e18 assets
According to getSharesByStake
, exploiter receives 100e18 * 10e18 / 20e18 = 50e18 shares
updateStrategyRewards
increases the totalStaked
by 5e18
StakingPool
now has 20e18 + 100e18 + 5e18 = 125e18 totalStaked
and 10e18 + 50e18 = 60e18 totalShares
Exploiter's 50e18 shares
are now worth 50e18 * 125e18 / 60e18 = 104166666666666666666 = 104.16e18 assets
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.