code-423n4 / 2024-01-salty-findings

11 stars 6 forks source link

Disproportionate Virtual Rewards for Early LP Providers #601

Closed c4-bot-9 closed 9 months ago

c4-bot-9 commented 9 months ago

Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingRewards.sol#L57 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingRewards.sol#L147 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingRewards.sol#L232 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/Upkeep.sol#L244

Vulnerability details

Vulnerability Details:

Liquidity providers in the protocol earn rewards for their contributions. These rewards are updated each time a user's share changes or they claim rewards. The _increaseUserShare function recalculates and assigns rewards whenever a user increases their stake.

function _increaseUserShare(address wallet, bytes32 poolID, uint256 increaseShareAmount, bool useCooldown)
        internal
    {
        ...
        uint256 existingTotalShares = totalShares[poolID];
        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);
        }
        ...
    }

The protocol uses the virtualRewards variable to account for any rewards before the user, this amount will be deducted when a user claims their rewards or decreases their stake.

Here, virtualRewardsToAdd is computed using the formula:

This computation can disproportionately affect early LPs, especially when their added amount is comparable to or larger than the existing total shares, leading to inflated virtual rewards and potentially denying them rewards for an extended period.

Example Scenario:

Consider the following:

The protocol can start with up to 5.5k rewards for every LP pool, that is the 1% daily emission. The upkeep function is responsible for transferring up to the maximum allowable daily rewards to the staking contract.

User 1 adds liquidity, receiving 5 shares.

User 2 follows, adding 10 shares. The virtualRewardsToAdd for User 2 would be:

Assume after several hours, additional users have joined, diluting User 2's share in the pool.

User 2 decides to claim their rewards through the claimAllRewards function, which internally utilizes the userRewardForPool function.

Here, even though the total rewards in the pool have increased to 100k, indicating that rewards have been earned, User 2's share of the rewards (10k) is still less than their inflated virtual rewards total of 11k.

Impact:

Early LPs will have their virtual rewards inflated, leading to a long duration where they cannot claim any rewards.

Proof Of Concept

function testInflatedVirtualRewards() public {
    bytes32 poolID1 = PoolUtils._poolID( wbtc, weth );
    bytes32[] memory poolIDs = new bytes32[](1);
    poolIDs[0] = poolID1;
    skip(2 days);
    uint256 depositedWBTC = ( 1 ether *10**8) / priceAggregator.getPriceBTC();
    uint256 depositedWETH = ( 1 ether *10**18) / priceAggregator.getPriceETH(); 
    (uint256 reserveWBTC, uint256 reserveWETH) = pools.getPoolReserves(wbtc, weth);
    upkeep.performUpkeep();
    // check total rewards of pool
    uint256[] memory rewardsbefore = new uint256[](1);
    rewardsbefore = collateralAndLiquidity.totalRewardsForPools(poolIDs);
    // Alice deposit collateral (first user)
    vm.startPrank(alice);
    collateralAndLiquidity.depositCollateralAndIncreaseShare( depositedWBTC, depositedWETH, 0, block.timestamp, false );
    vm.stopPrank();
    // bob deposit 10 times alice collateral
    _readyUser(bob);
    vm.startPrank(bob);
    collateralAndLiquidity.depositCollateralAndIncreaseShare( depositedWBTC * 10, depositedWETH * 10, 0, block.timestamp, false );
    vm.stopPrank();
    // check bobs virtual rewards
    uint virtualRewardsBob = collateralAndLiquidity.userVirtualRewardsForPool(bob, poolIDs[0]);
    // bobs virtual rewards is greater then all initial pool rewards
    assertTrue(virtualRewardsBob > rewardsbefore[0]);
    }

Tools Used:

Recommendation:

Invoking the performUpkeep function just before the initial distribution can help mitigate this issue. This action resets the timer, ensuring that rewards do not start at the maximum daily amount, thus preventing the overinflation of virtual rewards for early LPs.

Assessed type

Other

c4-judge commented 9 months ago

Picodes marked the issue as duplicate of #614

c4-judge commented 8 months ago

Picodes marked the issue as satisfactory

c4-judge commented 8 months ago

Picodes changed the severity to 3 (High Risk)