Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Position's fee growth can revert resulting in funds permanently locked #142

Closed
howlbot-integration bot opened this issue Sep 16, 2024 · 1 comment
Closed
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-46 🤖_54_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards sufficient quality report This report is of sufficient quality

Comments

@howlbot-integration
Copy link

Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/tick.rs#L125-L246

Vulnerability details

Impact

To determine the fee growth for a position, a function similar to the one used by Uniswap V3 is used. The new logic differs from the original logic, as it does not allow underflows.

Due to this, certain operations that depend on fee growth calculations may not execute properly and could revert (removing and adding liquidity), resulting in locked funds.

Context

Similar to Position's owed fees should allow underflow but it reverts instead, resulting in locked funds but with a different function/root cause, both issues must be fixed separately.

Proof of Concept

In tick.get_fee_growth_inside, Superposition's fee logic does not allow underflows:

    //@audit I've removed the testing-dbg flags to make it easier to read
    pub fn get_fee_growth_inside(
        &mut self,
        lower_tick: i32,
        upper_tick: i32,
        cur_tick: i32,
        fee_growth_global_0: &U256,
        fee_growth_global_1: &U256,
    ) -> Result<(U256, U256), Error> {
        // the fee growth inside this tick is the total fee
        // growth, minus the fee growth outside this tick
        let lower = self.ticks.get(lower_tick);
        let upper = self.ticks.get(upper_tick);

        let (fee_growth_below_0, fee_growth_below_1) = if cur_tick >= lower_tick {
            (
                lower.fee_growth_outside_0.get(),
                lower.fee_growth_outside_1.get(),
            )
        } else {
            (
                fee_growth_global_0
->                  .checked_sub(lower.fee_growth_outside_0.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
                fee_growth_global_1
->                  .checked_sub(lower.fee_growth_outside_1.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
            )
        };

        let (fee_growth_above_0, fee_growth_above_1) = if cur_tick < upper_tick {
            (
                upper.fee_growth_outside_0.get(),
                upper.fee_growth_outside_1.get(),
            )
        } else {
            (
                fee_growth_global_0
->                  .checked_sub(upper.fee_growth_outside_0.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
                fee_growth_global_1
->                  .checked_sub(upper.fee_growth_outside_1.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
            )
        };

        Ok((
            fee_growth_global_0
->              .checked_sub(fee_growth_below_0)
->              .and_then(|x| x.checked_sub(fee_growth_above_0))
                .ok_or(Error::FeeGrowthSubTick)?,
            fee_growth_global_1
->              .checked_sub(fee_growth_below_1)
->              .and_then(|x| x.checked_sub(fee_growth_above_1))
                .ok_or(Error::FeeGrowthSubTick)?,
        ))
    }

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/tick.rs#L125-L246

while the original Uniswap version allows underflows:

https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Tick.sol#L60-L95

The issue is that negative fees are expected due to how the formula works. It is explained in detail in this Uniswap's issue:
Uniswap/v3-core#573

This function is used every time a position is updated, so it will be impossible to remove funds from it when the underflow happens, resulting in locked funds.

Tools Used

Manual Review

Recommended Mitigation Steps

Use wrapping_sub instead, or a simple - operation, as it natively allows underflow in Rust release mode:

    pub fn get_fee_growth_inside(
        &mut self,
        lower_tick: i32,
        upper_tick: i32,
        cur_tick: i32,
        fee_growth_global_0: &U256,
        fee_growth_global_1: &U256,
    ) -> Result<(U256, U256), Error> {
        // the fee growth inside this tick is the total fee
        // growth, minus the fee growth outside this tick
        let lower = self.ticks.get(lower_tick);
        let upper = self.ticks.get(upper_tick);

        let (fee_growth_below_0, fee_growth_below_1) = if cur_tick >= lower_tick {
            (
                lower.fee_growth_outside_0.get(),
                lower.fee_growth_outside_1.get(),
            )
        } else {
            (
                fee_growth_global_0
-                   .checked_sub(lower.fee_growth_outside_0.get())
-                   .ok_or(Error::FeeGrowthSubTick)?,
+                   - lower.fee_growth_outside_0.get(),
                fee_growth_global_1
-                   .checked_sub(lower.fee_growth_outside_1.get())
-                   .ok_or(Error::FeeGrowthSubTick)?,
+					- lower.fee_growth_outside_1.get()
            )
        };

        let (fee_growth_above_0, fee_growth_above_1) = if cur_tick < upper_tick {
            (
                upper.fee_growth_outside_0.get(),
                upper.fee_growth_outside_1.get(),
            )
        } else {
            (
                fee_growth_global_0
-                   .checked_sub(upper.fee_growth_outside_0.get())
-                   .ok_or(Error::FeeGrowthSubTick)?,
+					- upper.fee_growth_outside_0.get()
                fee_growth_global_1
-                   .checked_sub(upper.fee_growth_outside_1.get())
-                   .ok_or(Error::FeeGrowthSubTick)?,
+					- upper.fee_growth_outside_1.get()
            )
        };

        Ok((
            fee_growth_global_0
-               .checked_sub(fee_growth_below_0)
-               .and_then(|x| x.checked_sub(fee_growth_above_0))
-               .ok_or(Error::FeeGrowthSubTick)?,
+				- fee_growth_below_0 - fee_growth_above_0
            fee_growth_global_1
-               .checked_sub(fee_growth_below_1)
-               .and_then(|x| x.checked_sub(fee_growth_above_1))
-               .ok_or(Error::FeeGrowthSubTick)?,
+				- fee_growth_below_1 - fee_growth_above_1
        ))
    }

Assessed type

Uniswap

@howlbot-integration howlbot-integration bot added 3 (High Risk) Assets can be stolen/lost/compromised directly 🤖_54_group AI based duplicate group recommendation bug Something isn't working duplicate-46 sufficient quality report This report is of sufficient quality labels Sep 16, 2024
howlbot-integration bot added a commit that referenced this issue Sep 16, 2024
@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Sep 23, 2024
@c4-judge
Copy link
Contributor

alex-ppg marked the issue as satisfactory

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-46 🤖_54_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards sufficient quality report This report is of sufficient quality
Projects
None yet
Development

No branches or pull requests

1 participant