The vulnerability arises due to the mechanism for calculating and distributing rewards in the context of newly created or newly emptied pools. In _increaseUserShare() within StakingRewards.sol, when a second user adds liquidity to a pool that has just become non-empty, the calculation of virtualRewardsToAdd disproportionately favors the first user who added liquidity. This is because the first user's virtualRewards remain at zero, enabling them to claim all the existing rewards in the pool, while subsequent users accrue virtualRewards that negate their share of these rewards.
if ( existingTotalShares != 0 ) // prevent / 0
{
// Round up in favor of the protocol.
uint256 virtualRewardsToAdd = Math.ceilDiv( totalRewards[poolID] * increaseShareAmount, existingTotalShares );
user.virtualRewards += uint128(virtualRewardsToAdd);
totalRewards[poolID] += uint128(virtualRewardsToAdd);
}
Flawed Reward Distribution Scenario
First User's Action: The first user adds liquidity to an empty pool. Their virtualRewards remain at zero.
Second User's Action: A second user adds an equal amount of liquidity. The increaseShareAmount/existingShares ratio equals 1, making virtualRewardsToAdd equal to totalRewards.
Reward Claim: When both users go to decrease their shares and claim rewards:
The first user, having no virtualRewards, can claim all the rewards in the pool.
The second user, whose virtualRewards equal the totalRewards, essentially has their claimable rewards negated, leaving them with nothing.
This mechanism leads to a situation where the first user to add liquidity to an empty pool can benefit from the existing rewards, while subsequent users, are unable to claim any rewards. Although this is the intended purpose of virtualRewards, this will manifest in 'empty pool hunters' who can frontrun transactions with a minimal amount of liquidity in order to be the first provider.
This can also lead to accounting issues as the first user is not concerned with setting a proper ratio to the tokens and instead is more focused on initial SALT rewards.
Lastly, if a user somehow maintains full control over the pool (is the only user) they can time withdraws and reentry such that they continue getting emitted rewards to themselves by:
1) Seeing a reward is incoming from the RewardEmitter.sol
2) Withdrawing all shares rendering existingTotalShares == 0 again
3) Letting the reward land into the now empty pool
4) Frontrunning and depositing a small amount of liquidity
Proof of Concept
Suppose a newly created pool has 100 SALT deposited into it. Then, the first user will be able to provide a minimal amount of liquidity while claiming the entire reward, leaving none for the second user.
Initialize virtualRewards for the First Depositor: When the first user adds liquidity to an empty pool, assign them a portion of the virtualRewards based on the total existing rewards in the pool to disincentivize reward hunting.
function _increaseUserShare( address wallet, bytes32 poolID, uint256 increaseShareAmount, bool useCooldown ) internal {
// ...
if (existingTotalShares == 0) {
// Initialize virtual rewards for the first user based on a predefined fraction of total rewards
uint256 initialVirtualRewards = totalRewards[poolID] * initialVirtualRewardFraction;
user.virtualRewards += uint128(initialVirtualRewards);
totalRewards[poolID] += uint128(initialVirtualRewards);
} else {
// Existing logic for subsequent users
uint256 virtualRewardsToAdd = Math.ceilDiv(totalRewards[poolID] * increaseShareAmount, existingTotalShares);
user.virtualRewards += uint128(virtualRewardsToAdd);
totalRewards[poolID] += uint128(virtualRewardsToAdd);
}
// ...
}
Lines of code
https://github.com/code-423n4/2024-01-salty/blob/main/src/staking/StakingRewards.sol#L78-L85
Vulnerability details
Impact
The vulnerability arises due to the mechanism for calculating and distributing rewards in the context of newly created or newly emptied pools. In
_increaseUserShare()
withinStakingRewards.sol
, when a second user adds liquidity to a pool that has just become non-empty, the calculation ofvirtualRewardsToAdd
disproportionately favors the first user who added liquidity. This is because the first user'svirtualRewards
remain at zero, enabling them to claim all the existing rewards in the pool, while subsequent users accruevirtualRewards
that negate their share of these rewards.Flawed Reward Distribution Scenario
virtualRewards
remain at zero.increaseShareAmount/existingShares
ratio equals 1, makingvirtualRewardsToAdd
equal tototalRewards
.virtualRewards
, can claim all the rewards in the pool.virtualRewards
equal thetotalRewards
, essentially has their claimable rewards negated, leaving them with nothing.This mechanism leads to a situation where the first user to add liquidity to an empty pool can benefit from the existing rewards, while subsequent users, are unable to claim any rewards. Although this is the intended purpose of
virtualRewards
, this will manifest in 'empty pool hunters' who can frontrun transactions with a minimal amount of liquidity in order to be the first provider.This can also lead to accounting issues as the first user is not concerned with setting a proper ratio to the tokens and instead is more focused on initial SALT rewards.
Lastly, if a user somehow maintains full control over the pool (is the only user) they can time withdraws and reentry such that they continue getting emitted rewards to themselves by:
1) Seeing a reward is incoming from the
RewardEmitter.sol
2) Withdrawing all shares renderingexistingTotalShares == 0
again 3) Letting the reward land into the now empty pool 4) Frontrunning and depositing a small amount of liquidityProof of Concept
Suppose a newly created pool has 100 SALT deposited into it. Then, the first user will be able to provide a minimal amount of liquidity while claiming the entire reward, leaving none for the second user.
Tools Used
foundry
Recommended Mitigation Steps
Initialize
virtualRewards
for the First Depositor: When the first user adds liquidity to an empty pool, assign them a portion of thevirtualRewards
based on the total existing rewards in the pool to disincentivize reward hunting.Assessed type
MEV