In the create_position function, there is no validation to check whether the provided lower tick is less than the upper tick, which is a crucial condition for ensuring proper tick range management.
When this validation is missing, it becomes possible for users to provide tick ranges where the lower tick is greater than the upper tick, leading to unexpected behaviors.
## Impact
The lack of validation for tick range order introduces potential inconsistencies in liquidity positions. When the ticks are inverted, the position behaves differently, affecting the token amounts that users must provide to establish or update their position.
This can also have some other issues too.
## Proof of Concept
```rust
pub fn update_position(&mut self, id: U256, delta: i128) -> Result<(I256, I256), Revert> {
// the pool must be enabled
assert_or!(self.enabled.get(), Error::PoolDisabled);
let position = self.positions.positions.get(id);
let lower = position.lower.get().sys();
let upper = position.upper.get().sys();
// update the ticks
let cur_tick = self.cur_tick.get().sys();
...
if delta != 0 {
flipped_lower = self.ticks.update(
@>> lower,
cur_tick,
..
)?;
flipped_upper = self.ticks.update(
@>> upper,
cur_tick,
..
)?;
...
}
...
// calculate liquidity change and the amount of each token we need
if delta != 0 {
let (amount_0, amount_1) =
@>> if self.cur_tick.get().sys() < lower { //@audit CASE - I
// 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(),
)
@>> } else if self.cur_tick.get().sys() < upper { //@audit CASE - II
// we're inside the range, the liquidity is active and we need both tokens
let new_liquidity = liquidity_math::add_delta(self.liquidity.get().sys(), delta)?;
self.liquidity.set(U128::lib(&new_liquidity));
(
sqrt_price_math::get_amount_0_delta(
self.sqrt_price.get(),
tick_math::get_sqrt_ratio_at_tick(upper)?,
delta,
)?,
sqrt_price_math::get_amount_1_delta(
tick_math::get_sqrt_ratio_at_tick(lower)?,
self.sqrt_price.get(),
delta,
)?,
)
@>> } else { //@audit CASE - III
// we're above the range, we need to move left, we'll need token1
(
I256::zero(),
sqrt_price_math::get_amount_1_delta(
tick_math::get_sqrt_ratio_at_tick(lower)?,
tick_math::get_sqrt_ratio_at_tick(upper)?,
delta,
)?,
)
};
Ok((amount_0, amount_1))
} else {
Ok((I256::zero(), I256::zero()))
}
}
Test to Update The position with delta +2000000
#[test]
fn test_similar_to_ethers() -> Result<(), Vec<u8>> {
test_utils::with_storage::<_, Pools, _>(
Some(address!("feb6034fc7df27df18a3a6bad5fb94c0d3dcb6d5").into_array()),
None, // slots map
None, // caller erc20 balances
None, // amm erc20 balances
|contract| {
// Create the storage
contract.ctor(msg::sender(), Address::ZERO, Address::ZERO)?;
let token_addr = address!("97392C28f02AF38ac2aC41AF61297FA2b269C3DE");
// First, we set up the pool.
contract.create_pool_D650_E2_D0(
token_addr,
test_utils::encode_sqrt_price(50, 1), // the price
0,
1,
100000000000,
)?;
contract.enable_pool_579_D_A658(token_addr, true)?;
let lower_tick = test_utils::encode_tick(50);
let upper_tick = test_utils::encode_tick(150);
let liquidity_delta = 2000000;
// Begin to create the position, following the same path as
// in `createPosition` in ethers-tests/tests.ts
contract.mint_position_B_C5_B086_D(token_addr, lower_tick, upper_tick)?;
let position_id = contract
.next_position_id
.clone()
.checked_sub(U256::one())
.unwrap();
let (a0, a1) = contract.update_position_C_7_F_1_F_740(
token_addr,
position_id,
liquidity_delta
)?;
Ok(())
},
)
}
Output:
Scene - 1 (Satisfies Case - 2 from the above snippet)
Lower Tick < Upper Tick (Lower = 50, Upper = 150)
// A0 => 119537
// A1 => 132
Scene - 2 (Satisfies Case - 1 from the above snippet)
Lower Tick > Upper Tick (Lower = 150, Upper = 50)
// A0 => 119540
// A1 => 0
Lines of code
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L75-L87
Vulnerability details
In the
create_position
function, there is no validation to check whether the provided lower tick is less than the upper tick, which is a crucial condition for ensuring proper tick range management.When this validation is missing, it becomes possible for users to provide tick ranges where the lower tick is greater than the upper tick, leading to unexpected behaviors.
@>> assert_or!(low >= min_tick && low <= max_tick, Error::InvalidTick); @>> assert_or!(up >= min_tick && up <= max_tick, Error::InvalidTick);
Test to Update The position with delta
+2000000
Output:
Scene - 2 (Satisfies Case - 1 from the above snippet) Lower Tick > Upper Tick (Lower = 150, Upper = 50) // A0 => 119540 // A1 => 0
Assessed type
Invalid Validation