As you can see, the lower and upper ticks are not checked if lower < upper, they are only checked if they are between min_tick and max_tick. Therefore, if min_tick = 0 and max_tick = 20, passing lower = 10 and upper = 5 will pass.
function checkTicks(int24 tickLower, int24 tickUpper) private pure {
if (tickLower >= tickUpper) revert TLU(); <------------------------
if (tickLower < TickMath.MIN_TICK) revert TLM();
if (tickUpper > TickMath.MAX_TICK) revert TUM();
}
This can lead to a lot of different problems, since it's something that might not even happen ever because UniswapV3Pool prevents it with this check, here are some problems we thought of:
When a position is updated, there are different cases based on the current tick and tick limits (tickLower and tickUpper) and based on these, how much of each token (token0, token1) is added to the pool is calculated.
pool.rs#L172-L245
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(),
)
} else if self.cur_tick.get().sys() < upper {
// 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)?;
#[cfg(feature = "testing-dbg")]
dbg!((
"update_position, cur_tick < upper path",
lower,
upper,
tick_math::get_sqrt_ratio_at_tick(lower)?.to_string(),
tick_math::get_sqrt_ratio_at_tick(upper)?.to_string(),
self.sqrt_price.get().to_string(),
delta,
self.liquidity.get().sys(),
new_liquidity
));
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 {
#[cfg(feature = "testing-dbg")]
dbg!((
"update_position, else",
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 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,
)?,
)
};
But if this is broken (i.e. lower > higher), token amount calculations will be completely wrong, resulting in incorrect liquidity additions.
Since the price is based on the current tick and where it is positioned between the lower and upper ticks, this can allow liquidity to be added at a lower or higher price than usual.
LPs can cheat this to only deposit one token (either token0 or token1) even if they need to deposit both, which if done will unbalance the pool and increase the price of the other token.
Lines of code
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L75-L87
Vulnerability details
Impact
Missing check allows the user to create a position with a
lowerTick > upperTick
.Proof of Concept
Position is created using
mint_position_B_C5_B086_D()
- https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/lib.rs#L495, andpool.create_position()
- https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L75-L87.As you can see, the lower and upper ticks are not checked if
lower < upper
, they are only checked if they are betweenmin_tick
andmax_tick
. Therefore, ifmin_tick
= 0 andmax_tick
= 20, passinglower
= 10 andupper
= 5 will pass.This should be checked when creating a position, as was done in
Uniswap
- https://github.com/Uniswap/v3-core/blob/6562c52e8f75f0c10f9deaf44861847585fc8129/contracts/UniswapV3Pool.sol#L316https://github.com/Uniswap/v3-core/blob/6562c52e8f75f0c10f9deaf44861847585fc8129/contracts/UniswapV3Pool.sol#L122-L126
This can lead to a lot of different problems, since it's something that might not even happen ever because
UniswapV3Pool
prevents it with this check, here are some problems we thought of:When a position is updated, there are different cases based on the current tick and tick limits (
tickLower
andtickUpper
) and based on these, how much of each token (token0
,token1
) is added to the pool is calculated. pool.rs#L172-L245But if this is broken (i.e.
lower
>higher
), token amount calculations will be completely wrong, resulting in incorrect liquidity additions.Since the price is based on the current tick and where it is positioned between the
lower
andupper
ticks, this can allow liquidity to be added at a lower or higher price than usual.LPs can cheat this to only deposit one token (either
token0
ortoken1
) even if they need to deposit both, which if done will unbalance the pool and increase the price of the other token.Tools Used
Manual Review
Recommended Mitigation Steps
Add a check like in UniswapV3Pool:
Assessed type
Uniswap