use UniswapV3.slot0 to get the value of sqrtPriceX96, which is used to perform the swap. However, the sqrtPriceX96 is pulled from Uniswap.slot0, which is the most recent data point and can be manipulated easily via MEV bots and Flashloans with sandwich attacks; which can cause the loss of funds when interacting with the Uniswap.swap function.
Proof of Concept
function _mintFullRange(
IUniswapV3Pool v3Pool,
address token0,
address token1,
uint24 fee
) internal returns (uint256, uint256) {
(uint160 currentSqrtPriceX96, , , , , , ) = v3Pool.slot0();
// For full range: L = Δx * sqrt(P) = Δy / sqrt(P)
// We start with fixed token amounts and apply this equation to calculate the liquidity
// Note that for pools with a tickSpacing that is not a power of 2 or greater than 8 (887272 % ts != 0),
// a position at the maximum and minimum allowable ticks will be wide, but not necessarily full-range.
// In this case, the `fullRangeLiquidity` will always be an underestimate in respect to the token amounts required to mint.
uint128 fullRangeLiquidity;
unchecked {
// Since we know one of the tokens is WETH, we simply add 0.1 ETH + worth in tokens
if (token0 == WETH) {
fullRangeLiquidity = uint128(
Math.mulDiv96RoundingUp(FULL_RANGE_LIQUIDITY_AMOUNT_WETH, currentSqrtPriceX96)
);
} else if (token1 == WETH) {
fullRangeLiquidity = uint128(
Math.mulDivRoundingUp(
FULL_RANGE_LIQUIDITY_AMOUNT_WETH,
Constants.FP96,
currentSqrtPriceX96
)
);
} else {
// Find the resulting liquidity for providing 1e6 of both tokens
uint128 liquidity0 = uint128(
Math.mulDiv96RoundingUp(FULL_RANGE_LIQUIDITY_AMOUNT_TOKEN, currentSqrtPriceX96)
);
uint128 liquidity1 = uint128(
Math.mulDivRoundingUp(
FULL_RANGE_LIQUIDITY_AMOUNT_TOKEN,
Constants.FP96,
currentSqrtPriceX96
)
);
// Pick the greater of the liquidities - i.e the more "expensive" option
// This ensures that the liquidity added is sufficiently large
fullRangeLiquidity = liquidity0 > liquidity1 ? liquidity0 : liquidity1;
}
}
// The maximum range we can mint is determined by the tickSpacing of the pool
// The upper and lower ticks must be divisible by `tickSpacing`, so
// tickSpacing = 1: tU/L = +/-887272
// tickSpacing = 10: tU/L = +/-887270
// tickSpacing = 60: tU/L = +/-887220
// tickSpacing = 200: tU/L = +/-887200
int24 tickLower;
int24 tickUpper;
unchecked {
int24 tickSpacing = v3Pool.tickSpacing();
tickLower = (Constants.MIN_V3POOL_TICK / tickSpacing) * tickSpacing;
tickUpper = -tickLower;
}
bytes memory mintCallback = abi.encode(
CallbackLib.CallbackData({
poolFeatures: CallbackLib.PoolFeatures({token0: token0, token1: token1, fee: fee}),
payer: msg.sender
})
);
return
IUniswapV3Pool(v3Pool).mint(
address(this),
tickLower,
tickUpper,
fullRangeLiquidity,
mintCallback
);
}
Tools Used
Manual Review
Recommended Mitigation Steps
To address this issue, avoid relying on slot0 and instead utilize Uniswap TWAP. Additionally, consider manually setting values for amountOutMin for swaps based on data acquired before repayment.
Lines of code
https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticFactory.sol#L335-L411
Vulnerability details
Impact
use UniswapV3.slot0 to get the value of sqrtPriceX96, which is used to perform the swap. However, the sqrtPriceX96 is pulled from Uniswap.slot0, which is the most recent data point and can be manipulated easily via MEV bots and Flashloans with sandwich attacks; which can cause the loss of funds when interacting with the Uniswap.swap function.
Proof of Concept
Tools Used
Manual Review
Recommended Mitigation Steps
To address this issue, avoid relying on slot0 and instead utilize Uniswap TWAP. Additionally, consider manually setting values for amountOutMin for swaps based on data acquired before repayment.
Assessed type
MEV