Skip to content

Commit

Permalink
refactor: update quoter (#32)
Browse files Browse the repository at this point in the history
* refactor: update quoter

* chore: resolve issues per comments
  • Loading branch information
chefburger authored Oct 15, 2024
1 parent 65e09a2 commit 6e2ac42
Show file tree
Hide file tree
Showing 13 changed files with 531 additions and 864 deletions.
35 changes: 19 additions & 16 deletions src/MixedQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
import {CurrencyLibrary, Currency, equals} from "pancake-v4-core/src/types/Currency.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {TickMath} from "pancake-v4-core/src/pool-cl/libraries/TickMath.sol";
import {IQuoter} from "./interfaces/IQuoter.sol";
import {ICLQuoter} from "./pool-cl/interfaces/ICLQuoter.sol";
import {IBinQuoter} from "./pool-bin/interfaces/IBinQuoter.sol";
import {IPancakeV3Pool} from "./interfaces/external/IPancakeV3Pool.sol";
Expand Down Expand Up @@ -155,11 +156,13 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback {
public
view
override
returns (uint256 amountOut)
returns (uint256 amountOut, uint256 gasEstimate)
{
uint256 gasBefore = gasleft();
(uint256 reserveIn, uint256 reserveOut) =
V3SmartRouterHelper.getReserves(factoryV2, params.tokenIn, params.tokenOut);
amountOut = V3SmartRouterHelper.getAmountOut(params.amountIn, reserveIn, reserveOut);
gasEstimate = gasBefore - gasleft();
}

/**
Expand All @@ -171,11 +174,13 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback {
public
view
override
returns (uint256 amountOut)
returns (uint256 amountOut, uint256 gasEstimate)
{
uint256 gasBefore = gasleft();
(uint256 i, uint256 j, address swapContract) =
V3SmartRouterHelper.getStableInfo(factoryStable, params.tokenIn, params.tokenOut, params.flag);
amountOut = IStableSwap(swapContract).get_dy(i, j, params.amountIn);
gasEstimate = gasBefore - gasleft();
}

/**
Expand All @@ -186,12 +191,13 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback {
bytes calldata actions,
bytes[] calldata params,
uint256 amountIn
) external override returns (uint256 amountOut) {
) external override returns (uint256 amountOut, uint256 gasEstimate) {
uint256 numActions = actions.length;
if (numActions == 0) revert NoActions();
if (numActions != params.length || numActions != paths.length - 1) revert InputLengthMismatch();

for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) {
uint256 gasEstimateForCurAction;
address tokenIn = paths[actionIndex];
address tokenOut = paths[actionIndex + 1];
if (tokenIn == tokenOut) revert InvalidPath();
Expand All @@ -200,14 +206,14 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback {
if (action == MixedQuoterActions.V2_EXACT_INPUT_SINGLE) {
(tokenIn, tokenOut) = convertNativeToWETH(tokenIn, tokenOut);
// params[actionIndex] is zero bytes
amountIn = quoteExactInputSingleV2(
(amountIn, gasEstimateForCurAction) = quoteExactInputSingleV2(
QuoteExactInputSingleV2Params({tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn})
);
} else if (action == MixedQuoterActions.V3_EXACT_INPUT_SINGLE) {
(tokenIn, tokenOut) = convertNativeToWETH(tokenIn, tokenOut);
// params[actionIndex]: abi.encode(fee)
uint24 fee = abi.decode(params[actionIndex], (uint24));
(uint256 _amountOut,,,) = quoteExactInputSingleV3(
(amountIn,,, gasEstimateForCurAction) = quoteExactInputSingleV3(
QuoteExactInputSingleV3Params({
tokenIn: tokenIn,
tokenOut: tokenOut,
Expand All @@ -216,42 +222,38 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback {
sqrtPriceLimitX96: 0
})
);
amountIn = _amountOut;
} else if (action == MixedQuoterActions.V4_CL_EXACT_INPUT_SINGLE) {
QuoteMixedV4ExactInputSingleParams memory clParams =
abi.decode(params[actionIndex], (QuoteMixedV4ExactInputSingleParams));
(tokenIn, tokenOut) = convertWETHToV4NativeCurency(clParams.poolKey, tokenIn, tokenOut);
bool zeroForOne = tokenIn < tokenOut;
checkV4PoolKeyCurrency(clParams.poolKey, zeroForOne, tokenIn, tokenOut);
(int128[] memory deltaAmounts,,) = clQuoter.quoteExactInputSingle(
ICLQuoter.QuoteExactSingleParams({
(amountIn, gasEstimateForCurAction) = clQuoter.quoteExactInputSingle(
IQuoter.QuoteExactSingleParams({
poolKey: clParams.poolKey,
zeroForOne: zeroForOne,
exactAmount: amountIn.toUint128(),
sqrtPriceLimitX96: 0,
hookData: clParams.hookData
})
);
amountIn = deltaAmounts[zeroForOne ? 1 : 0].toUint256();
} else if (action == MixedQuoterActions.V4_BIN_EXACT_INPUT_SINGLE) {
QuoteMixedV4ExactInputSingleParams memory binParams =
abi.decode(params[actionIndex], (QuoteMixedV4ExactInputSingleParams));
(tokenIn, tokenOut) = convertWETHToV4NativeCurency(binParams.poolKey, tokenIn, tokenOut);
bool zeroForOne = tokenIn < tokenOut;
checkV4PoolKeyCurrency(binParams.poolKey, zeroForOne, tokenIn, tokenOut);
(int128[] memory deltaAmounts,) = binQuoter.quoteExactInputSingle(
IBinQuoter.QuoteExactSingleParams({
(amountIn, gasEstimateForCurAction) = binQuoter.quoteExactInputSingle(
IQuoter.QuoteExactSingleParams({
poolKey: binParams.poolKey,
zeroForOne: zeroForOne,
exactAmount: amountIn.toUint128(),
hookData: binParams.hookData
})
);
amountIn = deltaAmounts[zeroForOne ? 1 : 0].toUint256();
} else if (action == MixedQuoterActions.SS_2_EXACT_INPUT_SINGLE) {
(tokenIn, tokenOut) = convertNativeToWETH(tokenIn, tokenOut);
// params[actionIndex] is zero bytes
amountIn = quoteExactInputSingleStable(
(amountIn, gasEstimateForCurAction) = quoteExactInputSingleStable(
QuoteExactInputSingleStableParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
Expand All @@ -262,7 +264,7 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback {
} else if (action == MixedQuoterActions.SS_3_EXACT_INPUT_SINGLE) {
(tokenIn, tokenOut) = convertNativeToWETH(tokenIn, tokenOut);
// params[actionIndex] is zero bytes
amountIn = quoteExactInputSingleStable(
(amountIn, gasEstimateForCurAction) = quoteExactInputSingleStable(
QuoteExactInputSingleStableParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
Expand All @@ -273,9 +275,10 @@ contract MixedQuoter is IMixedQuoter, IPancakeV3SwapCallback {
} else {
revert UnsupportedAction(action);
}
gasEstimate += gasEstimateForCurAction;
}

return amountIn;
return (amountIn, gasEstimate);
}

/// @dev Check if the poolKey currency matches the tokenIn and tokenOut
Expand Down
31 changes: 31 additions & 0 deletions src/base/BaseV4Quoter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2024 PancakeSwap
pragma solidity ^0.8.24;

import {SafeCallback} from "./SafeCallback.sol";
import {QuoterRevert} from "../libraries/QuoterRevert.sol";
import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol";
import {IQuoter} from "../interfaces/IQuoter.sol";
import {ILockCallback} from "pancake-v4-core/src/interfaces/ILockCallback.sol";
import {IPoolManager} from "../interfaces/IPoolManager.sol";

abstract contract BaseV4Quoter is SafeCallback, IQuoter {
using QuoterRevert for bytes;

constructor(address _poolManager) SafeCallback(IPoolManager(_poolManager).vault()) {}

/// @dev Only this address may call this function. Used to mimic internal functions, using an
/// external call to catch and parse revert reasons
modifier selfOnly() {
if (msg.sender != address(this)) revert NotSelf();
_;
}

function _lockAcquired(bytes calldata data) internal override returns (bytes memory) {
(bool success, bytes memory returnData) = address(this).call(data);
// Every quote path gathers a quote, and then reverts either with QuoteSwap(quoteAmount) or alternative error
if (success) revert UnexpectedCallSuccess();
// Bubble the revert string, whether a valid quote or an alternative error
returnData.bubbleReason();
}
}
52 changes: 0 additions & 52 deletions src/base/Quoter.sol

This file was deleted.

9 changes: 6 additions & 3 deletions src/interfaces/IMixedQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ interface IMixedQuoter {
/// V4_EXACT_INPUT_SINGLE params are encoded as `QuoteMixedV4ExactInputSingleParams`
/// @param amountIn The amount of the first token to swap
/// @return amountOut The amount of the last token that would be received
/// @return gasEstimate The estimate of the gas that the swap consumes
function quoteMixedExactInput(
address[] calldata paths,
bytes calldata actions,
bytes[] calldata params,
uint256 amountIn
) external returns (uint256 amountOut);
) external returns (uint256 amountOut, uint256 gasEstimate);

/// @notice Returns the amount out received for a given exact input but for a swap of a single pool
/// @param params The params for the quote, encoded as `QuoteExactInputSingleParams`
Expand All @@ -82,9 +83,10 @@ interface IMixedQuoter {
/// tokenOut The token being swapped out
/// amountIn The desired input amount
/// @return amountOut The amount of `tokenOut` that would be received
/// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactInputSingleV2(QuoteExactInputSingleV2Params memory params)
external
returns (uint256 amountOut);
returns (uint256 amountOut, uint256 gasEstimate);

/// @notice Returns the amount out received for a given exact input but for a swap of a single Stable pool
/// @param params The params for the quote, encoded as `QuoteExactInputSingleStableParams`
Expand All @@ -93,9 +95,10 @@ interface IMixedQuoter {
/// amountIn The desired input amount
/// flag The token amount in a single Stable pool. 2 for 2pool, 3 for 3pool
/// @return amountOut The amount of `tokenOut` that would be received
/// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactInputSingleStable(QuoteExactInputSingleStableParams memory params)
external
returns (uint256 amountOut);
returns (uint256 amountOut, uint256 gasEstimate);

/// @dev ExactOutput swaps are not supported by this new Quoter which is specialized for supporting routes
/// crossing Stable, V2 liquidity pairs and V3 pools.
Expand Down
19 changes: 11 additions & 8 deletions src/interfaces/IQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@ pragma solidity ^0.8.24;

import {Currency} from "pancake-v4-core/src/types/Currency.sol";
import {PathKey} from "../libraries/PathKey.sol";
import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
import {PoolId} from "pancake-v4-core/src/types/PoolId.sol";

/// @title IQuoter Interface
/// @notice Supports quoting the delta amounts from exact input or exact output swaps.
/// @dev These functions are not marked view because they rely on calling non-view functions and reverting
/// to compute the result. They are also not gas efficient and should not be called on-chain.
interface IQuoter {
error InvalidLockAcquiredSender();
error InsufficientAmountOut();
error LockFailure();
error NotEnoughLiquidity(PoolId poolId);
error NotSelf();
error UnexpectedRevertBytes(bytes revertData);
error UnexpectedCallSuccess();

struct QuoteExactSingleParams {
PoolKey poolKey;
bool zeroForOne;
uint128 exactAmount;
bytes hookData;
}

struct QuoteExactParams {
Currency exactCurrency;
PathKey[] path;
uint128 exactAmount;
}

function _quoteExactInput(QuoteExactParams memory params) external returns (bytes memory);

function _quoteExactOutput(QuoteExactParams memory params) external returns (bytes memory);
}
50 changes: 50 additions & 0 deletions src/libraries/QuoterRevert.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2024 PancakeSwap
pragma solidity ^0.8.24;

import {ParseBytes} from "pancake-v4-core/src/libraries/ParseBytes.sol";

library QuoterRevert {
using QuoterRevert for bytes;
using ParseBytes for bytes;

/// @notice error thrown when invalid revert bytes are thrown by the quote
error UnexpectedRevertBytes(bytes revertData);

/// @notice error thrown containing the quote as the data, to be caught and parsed later
error QuoteSwap(uint256 amount);

/// @notice reverts, where the revert data is the provided bytes
/// @dev called when quoting, to record the quote amount in an error
/// @dev QuoteSwap is used to differentiate this error from other errors thrown when simulating the swap
function revertQuote(uint256 quoteAmount) internal pure {
revert QuoteSwap(quoteAmount);
}

/// @notice reverts using the revertData as the reason
/// @dev to bubble up both the valid QuoteSwap(amount) error, or an alternative error thrown during simulation
function bubbleReason(bytes memory revertData) internal pure {
// mload(revertData): the length of the revert data
// add(revertData, 0x20): a pointer to the start of the revert data
assembly ("memory-safe") {
revert(add(revertData, 0x20), mload(revertData))
}
}

/// @notice validates whether a revert reason is a valid swap quote or not
/// if valid, it decodes the quote to return. Otherwise it reverts.
function parseQuoteAmount(bytes memory reason) internal pure returns (uint256 quoteAmount) {
// If the error doesnt start with QuoteSwap, we know this isnt a valid quote to parse
// Instead it is another revert that was triggered somewhere in the simulation
if (reason.parseSelector() != QuoteSwap.selector) {
revert UnexpectedRevertBytes(reason);
}

// reason -> reason+0x1f is the length of the reason string
// reason+0x20 -> reason+0x23 is the selector of QuoteSwap
// reason+0x24 -> reason+0x43 is the quoteAmount
assembly ("memory-safe") {
quoteAmount := mload(add(reason, 0x24))
}
}
}
Loading

0 comments on commit 6e2ac42

Please sign in to comment.