code-423n4 / 2024-08-superposition-validation

0 stars 0 forks source link

Missing check if liquidity is not increasing when updating positions #242

Closed c4-bot-7 closed 1 month ago

c4-bot-7 commented 1 month ago

Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L94-L255 https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/position.rs#L41-L72

Vulnerability details

Impact

Missing check allows users to create positions where they never deposit and never add liquidity.

Proof of Concept

When an user adds liquidity, it will call adjust_position() which will calculate the delta and then call update_position() where all the calculations are done and the position is updated.

But it is missing a check, if the delta is 0 and always increase the liquidity with delta.

position.rs#L41-L72

/// Updates a position, refreshing the amount of fees the position has earned and updating its
/// liquidity.
pub fn update(
    &mut self,
    id: U256,
    delta: i128,
    fee_growth_inside_0: U256,
    fee_growth_inside_1: U256,
) -> Result<(), Error> {
    let mut info = self.positions.setter(id);

    let owed_fees_0 = full_math::mul_div(
        fee_growth_inside_0
            .checked_sub(info.fee_growth_inside_0.get())
            .ok_or(Error::FeeGrowthSubPos)?,
        U256::from(info.liquidity.get()),
        full_math::Q128,
    )?;

    let owed_fees_1 = full_math::mul_div(
        fee_growth_inside_1
            .checked_sub(info.fee_growth_inside_1.get())
            .ok_or(Error::FeeGrowthSubPos)?,
        U256::from(info.liquidity.get()),
        full_math::Q128,
    )?;

    let liquidity_next = liquidity_math::add_delta(info.liquidity.get().sys(), delta)?; <---------------------------

    if delta != 0 {
        info.liquidity.set(U128::lib(&liquidity_next));
    }

This check is present in the Uniswap code and prevents the initiation of a position increase when the liquidity of the position does not change (ie adding 0 as delta).

https://github.com/Uniswap/v3-core/blob/6562c52e8f75f0c10f9deaf44861847585fc8129/contracts/libraries/Position.sol#L54-L55

function update(
    Info storage self,
    int128 liquidityDelta,
    uint256 feeGrowthInside0X128,
    uint256 feeGrowthInside1X128
) internal {
    Info memory _self = self;

    uint128 liquidityNext;
    if (liquidityDelta == 0) {
        if (_self.liquidity <= 0) revert NP(); // disallow pokes for 0 liquidity positions. <--------------------------------------------------
        liquidityNext = _self.liquidity;
    } else {
        liquidityNext = liquidityDelta < 0
            ? _self.liquidity - uint128(-liquidityDelta)
            : _self.liquidity + uint128(liquidityDelta);
    }

    ... MORE CODE

NOTE: Furthermore, one of the Main Invariants in the README mentioned that - Superposition should follow the UniswapV3 math faithfully, which is not the case here, violating the invariant. https://github.com/code-423n4/2024-08-superposition?tab=readme-ov-file#main-invariants

Tools Used

Manual Review

Recommended Mitigation Steps

Add a check preventing keeping position.liquidity = 0 (the code suggestion is not complete, of course).

/// Updates a position, refreshing the amount of fees the position has earned and updating its
/// liquidity.
pub fn update(
    &mut self,
    id: U256,
    delta: i128,
    fee_growth_inside_0: U256,
    fee_growth_inside_1: U256,
) -> Result<(), Error> {
    let mut info = self.positions.setter(id);

    let owed_fees_0 = full_math::mul_div(
        fee_growth_inside_0
            .checked_sub(info.fee_growth_inside_0.get())
            .ok_or(Error::FeeGrowthSubPos)?,
        U256::from(info.liquidity.get()),
        full_math::Q128,
    )?;

    let owed_fees_1 = full_math::mul_div(
        fee_growth_inside_1
            .checked_sub(info.fee_growth_inside_1.get())
            .ok_or(Error::FeeGrowthSubPos)?,
        U256::from(info.liquidity.get()),
        full_math::Q128,
    )?;

+       if delta == 0 && info.liquidity.get().sys() == 0 {
+           revert
+       }

    let liquidity_next = liquidity_math::add_delta(info.liquidity.get().sys(), delta)?; <---------------------------

    if delta != 0 {
        info.liquidity.set(U128::lib(&liquidity_next));
    }

Assessed type

Math