// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity =0.7.6; pragma abicoder v2; // TODO: change to import from @airdao/astra-cl-core import '@airdao/astra-cl-core/contracts/libraries/SafeCast.sol'; // TODO: change to import from @airdao/astra-cl-core import '@airdao/astra-cl-core/contracts/libraries/TickMath.sol'; // TODO: change to import from @airdao/astra-cl-core import '@airdao/astra-cl-core/contracts/libraries/TickBitmap.sol'; // TODO: change to import from @airdao/astra-cl-core import '@airdao/astra-cl-core/contracts/interfaces/IAstraCLPool.sol'; // TODO: change to import from @airdao/astra-cl-core import '@airdao/astra-cl-core/contracts/interfaces/callback/IAstraCLSwapCallback.sol'; import '../interfaces/IQuoterV2.sol'; import '../base/PeripheryImmutableState.sol'; import '../libraries/Path.sol'; import '../libraries/PoolAddress.sol'; import '../libraries/CallbackValidation.sol'; import '../libraries/PoolTicksCounter.sol'; /// @title Provides quotes for swaps /// @notice Allows getting the expected amount out or amount in for a given swap without executing the swap /// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute /// the swap and check the amounts in the callback. contract QuoterV2 is IQuoterV2, IAstraCLSwapCallback, PeripheryImmutableState { using Path for bytes; using SafeCast for uint256; using PoolTicksCounter for IAstraCLPool; /// @dev Transient storage variable used to check a safety condition in exact output swaps. uint256 private amountOutCached; constructor(address _factory, address _SAMB) PeripheryImmutableState(_factory, _SAMB) {} function getPool(address tokenA, address tokenB, uint24 fee) private view returns (IAstraCLPool) { return IAstraCLPool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee))); } /// @inheritdoc IAstraCLSwapCallback function astraCLSwapCallback(int256 amount0Delta, int256 amount1Delta, bytes memory path) external view override { require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); (bool isExactInput, uint256 amountToPay, uint256 amountReceived) = amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta)) : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta)); IAstraCLPool pool = getPool(tokenIn, tokenOut, fee); (uint160 sqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0(); if (isExactInput) { assembly { let ptr := mload(0x40) mstore(ptr, amountReceived) mstore(add(ptr, 0x20), sqrtPriceX96After) mstore(add(ptr, 0x40), tickAfter) revert(ptr, 96) } } else { // if the cache has been populated, ensure that the full output amount has been received if (amountOutCached != 0) require(amountReceived == amountOutCached); assembly { let ptr := mload(0x40) mstore(ptr, amountToPay) mstore(add(ptr, 0x20), sqrtPriceX96After) mstore(add(ptr, 0x40), tickAfter) revert(ptr, 96) } } } /// @dev Parses a revert reason that should contain the numeric quote function parseRevertReason( bytes memory reason ) private pure returns (uint256 amount, uint160 sqrtPriceX96After, int24 tickAfter) { if (reason.length != 96) { if (reason.length < 68) revert('Unexpected error'); assembly { reason := add(reason, 0x04) } revert(abi.decode(reason, (string))); } return abi.decode(reason, (uint256, uint160, int24)); } function handleRevert( bytes memory reason, IAstraCLPool pool, uint256 gasEstimate ) private view returns (uint256 amount, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256) { int24 tickBefore; int24 tickAfter; (, tickBefore, , , , , ) = pool.slot0(); (amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason); initializedTicksCrossed = pool.countInitializedTicksCrossed(tickBefore, tickAfter); return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate); } function quoteExactInputSingle( QuoteExactInputSingleParams memory params ) public override returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate) { bool zeroForOne = params.tokenIn < params.tokenOut; IAstraCLPool pool = getPool(params.tokenIn, params.tokenOut, params.fee); uint256 gasBefore = gasleft(); try pool.swap( address(this), // address(0) might cause issues with some tokens zeroForOne, params.amountIn.toInt256(), params.sqrtPriceLimitX96 == 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) : params.sqrtPriceLimitX96, abi.encodePacked(params.tokenIn, params.fee, params.tokenOut) ) {} catch (bytes memory reason) { gasEstimate = gasBefore - gasleft(); return handleRevert(reason, pool, gasEstimate); } } function quoteExactInput( bytes memory path, uint256 amountIn ) public override returns ( uint256 amountOut, uint160[] memory sqrtPriceX96AfterList, uint32[] memory initializedTicksCrossedList, uint256 gasEstimate ) { sqrtPriceX96AfterList = new uint160[](path.numPools()); initializedTicksCrossedList = new uint32[](path.numPools()); uint256 i = 0; while (true) { (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); // the outputs of prior swaps become the inputs to subsequent ones ( uint256 _amountOut, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate ) = quoteExactInputSingle( QuoteExactInputSingleParams({ tokenIn: tokenIn, tokenOut: tokenOut, fee: fee, amountIn: amountIn, sqrtPriceLimitX96: 0 }) ); sqrtPriceX96AfterList[i] = _sqrtPriceX96After; initializedTicksCrossedList[i] = _initializedTicksCrossed; amountIn = _amountOut; gasEstimate += _gasEstimate; i++; // decide whether to continue or terminate if (path.hasMultiplePools()) { path = path.skipToken(); } else { return (amountIn, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate); } } } function quoteExactOutputSingle( QuoteExactOutputSingleParams memory params ) public override returns (uint256 amountIn, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate) { bool zeroForOne = params.tokenIn < params.tokenOut; IAstraCLPool pool = getPool(params.tokenIn, params.tokenOut, params.fee); // if no price limit has been specified, cache the output amount for comparison in the swap callback if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.amount; uint256 gasBefore = gasleft(); try pool.swap( address(this), // address(0) might cause issues with some tokens zeroForOne, -params.amount.toInt256(), params.sqrtPriceLimitX96 == 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) : params.sqrtPriceLimitX96, abi.encodePacked(params.tokenOut, params.fee, params.tokenIn) ) {} catch (bytes memory reason) { gasEstimate = gasBefore - gasleft(); if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; // clear cache return handleRevert(reason, pool, gasEstimate); } } function quoteExactOutput( bytes memory path, uint256 amountOut ) public override returns ( uint256 amountIn, uint160[] memory sqrtPriceX96AfterList, uint32[] memory initializedTicksCrossedList, uint256 gasEstimate ) { sqrtPriceX96AfterList = new uint160[](path.numPools()); initializedTicksCrossedList = new uint32[](path.numPools()); uint256 i = 0; while (true) { (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool(); // the inputs of prior swaps become the outputs of subsequent ones ( uint256 _amountIn, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate ) = quoteExactOutputSingle( QuoteExactOutputSingleParams({ tokenIn: tokenIn, tokenOut: tokenOut, amount: amountOut, fee: fee, sqrtPriceLimitX96: 0 }) ); sqrtPriceX96AfterList[i] = _sqrtPriceX96After; initializedTicksCrossedList[i] = _initializedTicksCrossed; amountOut = _amountIn; gasEstimate += _gasEstimate; i++; // decide whether to continue or terminate if (path.hasMultiplePools()) { path = path.skipToken(); } else { return (amountOut, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate); } } } }