[api] chore: refactor MultiQuoters to use abstract contract [LIT-910] (#167)

This commit is contained in:
Savarn Dontamsetti (Sav)
2023-03-16 12:50:26 -04:00
committed by GitHub
parent 09c8a22a62
commit 3cb7ea9980
19 changed files with 608 additions and 818 deletions

View File

@@ -21,6 +21,7 @@ pragma solidity >=0.6;
pragma experimental ABIEncoderV2;
import "./interfaces/IAlgebra.sol";
import "./interfaces/IMultiQuoter.sol";
contract AlgebraCommon {
function toAlgebraPath(address[] memory tokenPath) internal pure returns (bytes memory algebraPath) {
@@ -42,10 +43,10 @@ contract AlgebraCommon {
}
}
function isValidTokenPath(IAlgebraFactory factory, address[] memory tokenPath) internal view returns (bool) {
function isValidTokenPath(address factory, address[] memory tokenPath) internal view returns (bool) {
for (uint256 i = 0; i < tokenPath.length - 1; ++i) {
IAlgebraPool pool = factory.poolByPair(tokenPath[i], tokenPath[i + 1]);
if (address(pool) == address(0)) {
address pool = IAlgebraFactory(factory).poolByPair(tokenPath[i], tokenPath[i + 1]);
if (pool == address(0)) {
return false;
}
}

View File

@@ -23,13 +23,9 @@ import "@cryptoalgebra/core/contracts/libraries/LiquidityMath.sol";
import "@cryptoalgebra/periphery/contracts/libraries/Path.sol";
import "./interfaces/IAlgebra.sol";
import "./MultiQuoter.sol";
contract AlgebraMultiQuoter is IAlgebraMultiQuoter {
// TODO: both quoteExactMultiInput and quoteExactMultiOutput revert at the end of the quoting logic
// and return results encodied into a revert reason. The revert should be removed and replaced with
// a normal return statement whenever UniswapV3Sampler and KyberElasticSampler stop having the
// two pool filtering logic. AlgebraMultiQuoter has the same revert logic in order to have a common
// interface across tick based AMM MultiQuoters.
contract AlgebraMultiQuoter is MultiQuoter {
using SafeCast for uint256;
using LowGasSafeMath for int256;
using Path for bytes;
@@ -72,173 +68,37 @@ contract AlgebraMultiQuoter is IAlgebraMultiQuoter {
uint256 gasBefore;
}
// the result of multiswap
struct MultiSwapResult {
// the gas estimate for each of swap amounts
uint256[] gasEstimates;
// the token0 delta for each swap amount, positive indicates sent and negative indicates receipt
int256[] amounts0;
// the token1 delta for each swap amount, positive indicates sent and negative indicates receipt
int256[] amounts1;
}
/// @notice Quotes amounts out for a set of exact input swaps and provides results encoded into a revert reason
/// @param factory The factory contract managing Algebra pools
/// @param path The path of the swap, i.e. each token pair and the pool fee
/// @param amountsIn The amounts in of the first token to swap
/// @dev This function reverts at the end of the quoting logic and encodes (uint256[] amountsOut, uint256[] gasEstimates)
/// into the revert reason. See additional documentation below.
function quoteExactMultiInput(
IAlgebraFactory factory,
/// @inheritdoc MultiQuoter
function getFirstSwapDetails(
address factory,
bytes memory path,
uint256[] memory amountsIn
) public view override {
for (uint256 i = 0; i < amountsIn.length - 1; ++i) {
require(amountsIn[i] <= amountsIn[i + 1], "AlgebraMultiQuoter/amountsIn must be monotonically increasing");
bool isExactInput
) internal view override returns (address pool, bool zeroForOne) {
address tokenIn;
address tokenOut;
if (isExactInput) {
(tokenIn, tokenOut) = path.decodeFirstPool();
} else {
(tokenOut, tokenIn) = path.decodeFirstPool();
}
uint256[] memory gasEstimates = new uint256[](amountsIn.length);
while (true) {
(address tokenIn, address tokenOut) = path.decodeFirstPool();
bool zeroForOne = tokenIn < tokenOut;
IAlgebraPool pool = factory.poolByPair(tokenIn, tokenOut);
// multiswap only accepts int256[] for input amounts
int256[] memory amounts = new int256[](amountsIn.length);
for (uint256 i = 0; i < amountsIn.length; ++i) {
amounts[i] = int256(amountsIn[i]);
}
MultiSwapResult memory result = multiswap(
pool,
zeroForOne,
amounts,
zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1
);
for (uint256 i = 0; i < amountsIn.length; ++i) {
amountsIn[i] = zeroForOne ? uint256(-result.amounts1[i]) : uint256(-result.amounts0[i]);
gasEstimates[i] += result.gasEstimates[i];
}
//decide whether to continue or terminate
if (path.hasMultiplePools()) {
path = path.skipToken();
} else {
// quote results must be encoded into a revert because otherwise subsequent calls
// to AlgebraMultiQuoter result in multiswap hitting pool storage slots that are
// already warm. This results in very inaccurate gas estimates when estimating gas
// usage for settlement.
bytes memory revertResult = abi.encodeWithSignature(
"result(uint256[],uint256[])",
amountsIn,
gasEstimates
);
assembly {
revert(add(revertResult, 0x20), mload(revertResult))
}
}
}
zeroForOne = tokenIn < tokenOut;
pool = IAlgebraFactory(factory).poolByPair(tokenIn, tokenOut);
}
/// @notice Quotes amounts in a set of exact output swaps and provides results encoded into a revert reason
/// @param factory The factory contract managing Algebra pools
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
/// @param amountsOut The amounts out of the last token to receive
/// @dev This function reverts at the end of the quoting logic and encodes (uint256[] amountsIn, uint256[] gasEstimates)
/// into the revert reason. See additional documentation below.
function quoteExactMultiOutput(
IAlgebraFactory factory,
bytes memory path,
uint256[] memory amountsOut
) public view override {
uint256[] memory amountsIn = new uint256[](amountsOut.length);
uint256[] memory gasEstimates = new uint256[](amountsOut.length);
for (uint256 i = 0; i < amountsOut.length - 1; ++i) {
require(
amountsOut[i] <= amountsOut[i + 1],
"AlgebraMultiQuoter/amountsOut must be monotonically increasing"
);
}
uint256 nextAmountsLength = amountsOut.length;
while (true) {
(address tokenOut, address tokenIn) = path.decodeFirstPool();
bool zeroForOne = tokenIn < tokenOut;
IAlgebraPool pool = factory.poolByPair(tokenIn, tokenOut);
// multiswap only accepts int256[] for output amounts
int256[] memory amounts = new int256[](nextAmountsLength);
for (uint256 i = 0; i < nextAmountsLength; ++i) {
amounts[i] = -int256(amountsOut[i]);
}
MultiSwapResult memory result = multiswap(
pool,
zeroForOne,
amounts,
zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1
);
for (uint256 i = 0; i < nextAmountsLength; ++i) {
uint256 amountReceived = zeroForOne ? uint256(-result.amounts1[i]) : uint256(-result.amounts0[i]);
if (amountReceived != amountsOut[i]) {
// for exact output swaps we need to check whether we would receive the full amount due to
// multiswap behavior when hitting the limit
nextAmountsLength = i;
break;
} else {
// populate amountsOut for the next pool
amountsOut[i] = zeroForOne ? uint256(result.amounts0[i]) : uint256(result.amounts1[i]);
gasEstimates[i] += result.gasEstimates[i];
}
}
if (nextAmountsLength == 0 || !path.hasMultiplePools()) {
for (uint256 i = 0; i < nextAmountsLength; ++i) {
amountsIn[i] = amountsOut[i];
}
// quote results must be encoded into a revert because otherwise subsequent calls
// to UniswapV3MultiQuoter result in multiswap hitting pool storage slots that are
// already warm. This results in very inaccurate gas estimates when estimating gas
// usage for settlement.
bytes memory revertResult = abi.encodeWithSignature(
"result(uint256[],uint256[])",
amountsIn,
gasEstimates
);
assembly {
revert(add(revertResult, 0x20), mload(revertResult))
}
}
path = path.skipToken();
}
}
/// @notice swap multiple amounts of token0 for token1 or token1 for token1
/// @dev The results of multiswap includes slight rounding issues resulting from rounding up/rounding down in SqrtPriceMath library. Additionally,
/// it should be noted that multiswap requires a monotonically increasing list of amounts for exact inputs and monotonically decreasing list of
/// amounts for exact outputs.
/// @param pool The Algebra pool to simulate each of the swap amounts for
/// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0
/// @param amounts The amounts of the swaps, positive values indicate exactInput and negative values indicate exact output
/// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this
/// value after the swap. If one for zero, the price cannot be greater than this value after the swap
/// @return result The results of the swap as a MultiSwapResult struct with gas used, token0 and token1 deltas
/// @inheritdoc MultiQuoter
function multiswap(
IAlgebraPool pool,
address p,
bool zeroForOne,
int256[] memory amounts,
uint160 sqrtPriceLimitX96
) private view returns (MultiSwapResult memory result) {
int256[] memory amounts
) internal view override returns (MultiSwapResult memory result) {
result.gasEstimates = new uint256[](amounts.length);
result.amounts0 = new int256[](amounts.length);
result.amounts1 = new int256[](amounts.length);
uint160 sqrtPriceLimitX96 = zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1;
IAlgebraPool pool = IAlgebraPool(p);
(uint160 sqrtPriceX96Start, int24 tickStart, uint16 fee, , , , ) = pool.globalState();
int24 tickSpacing = pool.tickSpacing();
@@ -463,4 +323,14 @@ contract AlgebraMultiQuoter is IAlgebraMultiQuoter {
}
return (getSingleSignificantBit(word));
}
/// @inheritdoc MultiQuoter
function pathHasMultiplePools(bytes memory path) internal pure override returns (bool) {
return path.hasMultiplePools();
}
/// @inheritdoc MultiQuoter
function pathSkipToken(bytes memory path) internal pure override returns (bytes memory) {
return path.skipToken();
}
}

View File

@@ -21,7 +21,7 @@ pragma solidity ^0.6;
pragma experimental ABIEncoderV2;
import "./AlgebraCommon.sol";
import "./interfaces/IAlgebra.sol";
import "./interfaces/IMultiQuoter.sol";
contract AlgebraSampler is AlgebraCommon {
/// @dev Sample sell quotes from Algebra.
@@ -33,8 +33,8 @@ contract AlgebraSampler is AlgebraCommon {
/// @return gasEstimates Estimated amount of gas used
/// @return outputAmounts Maker amounts bought at each taker token amount.
function sampleSellsFromAlgebra(
IAlgebraMultiQuoter quoter,
IAlgebraFactory factory,
IMultiQuoter quoter,
address factory,
address[] memory tokenPath,
uint256[] memory inputAmounts
) public view returns (bytes memory path, uint256[] memory gasEstimates, uint256[] memory outputAmounts) {
@@ -60,8 +60,8 @@ contract AlgebraSampler is AlgebraCommon {
/// @return gasEstimates Estimated amount of gas used
/// @return inputAmounts Taker amounts sold at each maker token amount.
function sampleBuysFromAlgebra(
IAlgebraMultiQuoter quoter,
IAlgebraFactory factory,
IMultiQuoter quoter,
address factory,
address[] memory tokenPath,
uint256[] memory outputAmounts
) public view returns (bytes memory path, uint256[] memory gasEstimates, uint256[] memory inputAmounts) {

View File

@@ -10,10 +10,10 @@ contract KyberElasticCommon {
/// @dev Returns `poolPaths` to sample against. The caller is responsible for not using path involving zero address(es).
function _getPoolPaths(
IMultiQuoter quoter,
IFactory factory,
address factory,
address[] memory path,
uint256 inputAmount
) internal view returns (IPool[][] memory poolPaths) {
) internal view returns (IKyberElasticPool[][] memory poolPaths) {
if (path.length == 2) {
return _getPoolPathSingleHop(quoter, factory, path, inputAmount);
}
@@ -25,17 +25,17 @@ contract KyberElasticCommon {
function _getPoolPathSingleHop(
IMultiQuoter quoter,
IFactory factory,
address factory,
address[] memory path,
uint256 inputAmount
) internal view returns (IPool[][] memory poolPaths) {
poolPaths = new IPool[][](2);
(IPool[2] memory topPools, ) = _getTopTwoPools(quoter, factory, path[0], path[1], inputAmount);
) internal view returns (IKyberElasticPool[][] memory poolPaths) {
poolPaths = new IKyberElasticPool[][](2);
(IKyberElasticPool[2] memory topPools, ) = _getTopTwoPools(quoter, factory, path[0], path[1], inputAmount);
uint256 pathCount = 0;
for (uint256 i = 0; i < 2; i++) {
IPool topPool = topPools[i];
poolPaths[pathCount] = new IPool[](1);
IKyberElasticPool topPool = topPools[i];
poolPaths[pathCount] = new IKyberElasticPool[](1);
poolPaths[pathCount][0] = topPool;
pathCount++;
}
@@ -43,25 +43,31 @@ contract KyberElasticCommon {
function _getPoolPathTwoHop(
IMultiQuoter quoter,
IFactory factory,
address factory,
address[] memory path,
uint256 inputAmount
) internal view returns (IPool[][] memory poolPaths) {
poolPaths = new IPool[][](4);
(IPool[2] memory firstHopTopPools, uint256[2] memory firstHopAmounts) = _getTopTwoPools(
) internal view returns (IKyberElasticPool[][] memory poolPaths) {
poolPaths = new IKyberElasticPool[][](4);
(IKyberElasticPool[2] memory firstHopTopPools, uint256[2] memory firstHopAmounts) = _getTopTwoPools(
quoter,
factory,
path[0],
path[1],
inputAmount
);
(IPool[2] memory secondHopTopPools, ) = _getTopTwoPools(quoter, factory, path[1], path[2], firstHopAmounts[0]);
(IKyberElasticPool[2] memory secondHopTopPools, ) = _getTopTwoPools(
quoter,
factory,
path[1],
path[2],
firstHopAmounts[0]
);
uint256 pathCount = 0;
for (uint256 i = 0; i < 2; i++) {
for (uint256 j = 0; j < 2; j++) {
poolPaths[pathCount] = new IPool[](2);
IPool[] memory currentPath = poolPaths[pathCount];
poolPaths[pathCount] = new IKyberElasticPool[](2);
IKyberElasticPool[] memory currentPath = poolPaths[pathCount];
currentPath[0] = firstHopTopPools[i];
currentPath[1] = secondHopTopPools[j];
pathCount++;
@@ -73,11 +79,11 @@ contract KyberElasticCommon {
/// Addresses in `topPools` can be zero addresses when there are pool isn't available.
function _getTopTwoPools(
IMultiQuoter multiQuoter,
IFactory factory,
address factory,
address inputToken,
address outputToken,
uint256 inputAmount
) internal view returns (IPool[2] memory topPools, uint256[2] memory outputAmounts) {
) internal view returns (IKyberElasticPool[2] memory topPools, uint256[2] memory topOutputAmounts) {
address[] memory tokenPath = new address[](2);
tokenPath[0] = inputToken;
tokenPath[1] = outputToken;
@@ -87,33 +93,38 @@ contract KyberElasticCommon {
uint16[5] memory validPoolFees = [uint16(8), uint16(10), uint16(40), uint16(300), uint16(1000)]; // in bps
for (uint256 i = 0; i < validPoolFees.length; ++i) {
IPool pool = IPool(factory.getPool(inputToken, outputToken, validPoolFees[i]));
IKyberElasticPool pool = IKyberElasticPool(
IKyberElasticFactory(factory).getPool(inputToken, outputToken, validPoolFees[i])
);
if (!_isValidPool(pool)) {
continue;
}
IPool[] memory poolPath = new IPool[](1);
IKyberElasticPool[] memory poolPath = new IKyberElasticPool[](1);
poolPath[0] = pool;
bytes memory dexPath = _toPath(tokenPath, poolPath);
try
multiQuoter.quoteExactMultiInput{gas: POOL_FILTERING_QUOTE_GAS}(factory, dexPath, inputAmounts)
returns (uint256[] memory amountsOut, uint256[] memory /* gasEstimate */) {
// Keeping track of the top 2 pools.
if (amountsOut[0] > outputAmounts[0]) {
outputAmounts[1] = outputAmounts[0];
topPools[1] = topPools[0];
outputAmounts[0] = amountsOut[0];
topPools[0] = pool;
} else if (amountsOut[0] > outputAmounts[1]) {
outputAmounts[1] = amountsOut[0];
topPools[1] = pool;
{} catch (bytes memory reason) {
(bool success, uint256[] memory outputAmounts, ) = catchKyberElasticMultiSwapResult(reason);
if (success) {
// Keeping track of the top 2 pools.
if (outputAmounts[0] > topOutputAmounts[0]) {
topOutputAmounts[1] = topOutputAmounts[0];
topPools[1] = topPools[0];
topOutputAmounts[0] = outputAmounts[0];
topPools[0] = pool;
} else if (outputAmounts[0] > topOutputAmounts[1]) {
topOutputAmounts[1] = outputAmounts[0];
topPools[1] = pool;
}
}
} catch {}
}
}
}
function _isValidPool(IPool pool) internal view returns (bool isValid) {
function _isValidPool(IKyberElasticPool pool) internal view returns (bool isValid) {
// Check if it has been deployed.
uint256 codeSize;
assembly {
@@ -122,7 +133,7 @@ contract KyberElasticCommon {
return codeSize != 0;
}
function _isValidPoolPath(IPool[] memory poolPaths) internal pure returns (bool) {
function _isValidPoolPath(IKyberElasticPool[] memory poolPaths) internal pure returns (bool) {
for (uint256 i = 0; i < poolPaths.length; i++) {
if (address(poolPaths[i]) == address(0)) {
return false;
@@ -131,7 +142,10 @@ contract KyberElasticCommon {
return true;
}
function _toPath(address[] memory tokenPath, IPool[] memory poolPath) internal view returns (bytes memory path) {
function _toPath(
address[] memory tokenPath,
IKyberElasticPool[] memory poolPath
) internal view returns (bytes memory path) {
require(tokenPath.length >= 2 && tokenPath.length == poolPath.length + 1, "invalid path lengths");
// paths are tightly packed as:
// [token0, token0token1PairFee, token1, token1Token2PairFee, token2, ...]
@@ -163,10 +177,33 @@ contract KyberElasticCommon {
}
}
function _reversePoolPath(IPool[] memory poolPath) internal pure returns (IPool[] memory reversed) {
reversed = new IPool[](poolPath.length);
function _reversePoolPath(
IKyberElasticPool[] memory poolPath
) internal pure returns (IKyberElasticPool[] memory reversed) {
reversed = new IKyberElasticPool[](poolPath.length);
for (uint256 i = 0; i < poolPath.length; ++i) {
reversed[i] = poolPath[poolPath.length - i - 1];
}
}
function catchKyberElasticMultiSwapResult(
bytes memory revertReason
) internal pure returns (bool success, uint256[] memory amounts, uint256[] memory gasEstimates) {
bytes4 selector;
assembly {
selector := mload(add(revertReason, 32))
}
if (selector != bytes4(keccak256("result(uint256[],uint256[])"))) {
return (false, amounts, gasEstimates);
}
assembly {
let length := sub(mload(revertReason), 4)
revertReason := add(revertReason, 4)
mstore(revertReason, length)
}
(amounts, gasEstimates) = abi.decode(revertReason, (uint256[], uint256[]));
return (true, amounts, gasEstimates);
}
}

View File

@@ -20,8 +20,6 @@
// transitive dependencies PathHelper.sol and BytesLib.sol require 0.8.9
pragma solidity 0.8.9;
import "./libraries/Multiswap.sol";
import "./interfaces/IMultiQuoter.sol";
import "@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol";
import "@kybernetwork/ks-elastic-sc/contracts/libraries/TickMath.sol";
import "@kybernetwork/ks-elastic-sc/contracts/libraries/SwapMath.sol";
@@ -29,12 +27,14 @@ import "@kybernetwork/ks-elastic-sc/contracts/libraries/Linkedlist.sol";
import "@kybernetwork/ks-elastic-sc/contracts/libraries/LiqDeltaMath.sol";
import "@kybernetwork/ks-elastic-sc/contracts/periphery/libraries/PathHelper.sol";
/// @title Provides quotes for multiple swap amounts
/// @notice Allows getting the expected amount out or amount in for multiple given swap amounts without executing the swap
contract KyberElasticMultiQuoter is IMultiQuoter {
import "./interfaces/IKyberElastic.sol";
import "./MultiQuoter.sol";
contract KyberElasticMultiQuoter is MultiQuoter {
using SafeCast for uint256;
using LowGasSafeMath for int256;
using SafeCast for int128;
using PathHelper for bytes;
// temporary swap variables, some of which will be used to update the pool state
struct SwapData {
@@ -44,7 +44,7 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
int24 currentTick; // the tick associated with the current price
int24 nextTick; // the next initialized tick
uint160 nextSqrtP; // the price of nextTick
bool isToken0; // true if specifiedAmount is in token0, false if in token1
bool zeroForOne; // true if swap is token0 for token 1
bool isExactInput; // true = input qty, false = output qty
uint128 baseL; // the cached base pool liquidity without reinvestment liquidity
uint128 reinvestL; // the cached reinvestment liquidity
@@ -53,94 +53,15 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
uint256 gasResidual; // amount of gas used between current tick and current price
}
// TODO: same as uniswapv3 multiquoter logic. see if we can make generic.
function quoteExactMultiInput(
IFactory factory,
/// @inheritdoc MultiQuoter
function getFirstSwapDetails(
address factory,
bytes memory path,
uint256[] memory amountsIn
) public view returns (uint256[] memory amountsOut, uint256[] memory gasEstimate) {
for (uint256 i = 1; i < amountsIn.length; ++i) {
require(amountsIn[i] >= amountsIn[i - 1], "multiswap amounts must be monotonically increasing");
}
gasEstimate = new uint256[](amountsIn.length);
while (true) {
(address tokenIn, address tokenOut, uint24 fee) = PathHelper.decodeFirstPool(path);
// NOTE: this is equivalent to UniswapV3's zeroForOne.
// if tokenIn < tokenOut, token input and specified token is token0, swap from 0 to 1
bool isToken0 = tokenIn < tokenOut;
IPool pool = IPool(factory.getPool(tokenIn, tokenOut, fee));
// multiswap only accepts int256[] for input amounts
int256[] memory amounts = new int256[](amountsIn.length);
for (uint256 i = 0; i < amountsIn.length; ++i) {
amounts[i] = int256(amountsIn[i]);
}
Multiswap.Result memory result = multiswap(
pool,
isToken0,
amounts,
isToken0 ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1
);
for (uint256 i = 0; i < amountsIn.length; ++i) {
amountsIn[i] = isToken0 ? uint256(-result.amounts1[i]) : uint256(-result.amounts0[i]);
gasEstimate[i] += result.gasEstimates[i];
}
// decide whether to continue or terminate
if (PathHelper.hasMultiplePools(path)) {
path = PathHelper.skipToken(path);
} else {
return (amountsIn, gasEstimate);
}
}
}
// TODO: same as uniswapv3 multiquoter logic. see if we can make generic.
function quoteExactMultiOutput(
IFactory factory,
bytes memory path,
uint256[] memory amountsOut
) public view returns (uint256[] memory amountsIn, uint256[] memory gasEstimate) {
for (uint256 i = 1; i < amountsOut.length; ++i) {
require(amountsOut[i] >= amountsOut[i - 1], "multiswap amounts must be monotonically inreasing");
}
gasEstimate = new uint256[](amountsOut.length);
while (true) {
(address tokenOut, address tokenIn, uint24 fee) = PathHelper.decodeFirstPool(path);
// NOTE: this is equivalent to UniswapV3's zeroForOne.
// if tokenIn > tokenOut, output token and specified token is token0, swap from token1 to token0
bool isToken0 = tokenIn > tokenOut;
IPool pool = IPool(factory.getPool(tokenIn, tokenOut, fee));
// multiswap only accepts int256[] for input amounts
int256[] memory amounts = new int256[](amountsOut.length);
for (uint256 i = 0; i < amountsOut.length; ++i) {
amounts[i] = -int256(amountsOut[i]);
}
Multiswap.Result memory result = multiswap(
pool,
isToken0,
amounts,
isToken0 ? TickMath.MAX_SQRT_RATIO - 1 : TickMath.MIN_SQRT_RATIO + 1
);
for (uint256 i = 0; i < amountsOut.length; ++i) {
amountsOut[i] = isToken0 ? uint256(result.amounts0[i]) : uint256(result.amounts1[i]);
gasEstimate[i] += result.gasEstimates[i];
}
// decide whether to continue or terminate
if (PathHelper.hasMultiplePools(path)) {
path = PathHelper.skipToken(path);
} else {
return (amountsOut, gasEstimate);
}
}
bool isExactInput
) internal view override returns (address pool, bool zeroForOne) {
(address tokenA, address tokenB, uint24 fee) = path.decodeFirstPool();
zeroForOne = tokenA < tokenB;
pool = IKyberElasticFactory(factory).getPool(tokenA, tokenB, fee);
}
/// @dev Return initial data before swapping
@@ -151,7 +72,7 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
/// @return currentTick current pool tick
/// @return nextTick next tick to calculate data
function _getInitialSwapData(
IPool pool,
IKyberElasticPool pool,
bool willUpTick
) internal view returns (uint128 baseL, uint128 reinvestL, uint160 sqrtP, int24 currentTick, int24 nextTick) {
(sqrtP, currentTick, nextTick, ) = pool.getPoolState();
@@ -163,25 +84,27 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
}
}
/// @notice swap multiple amounts of token0 for token1 or token1 for token0
/// @inheritdoc MultiQuoter
function multiswap(
IPool pool,
bool isToken0,
int256[] memory amounts,
uint160 limitSqrtP
) private view returns (Multiswap.Result memory result) {
address p,
bool zeroForOne,
int256[] memory amounts
) internal view override returns (MultiSwapResult memory result) {
result.gasEstimates = new uint256[](amounts.length);
result.amounts0 = new int256[](amounts.length);
result.amounts1 = new int256[](amounts.length);
uint256 gasBefore = gasleft();
IKyberElasticPool pool = IKyberElasticPool(p);
SwapData memory swapData;
swapData.specifiedAmount = amounts[0];
swapData.isToken0 = isToken0;
swapData.zeroForOne = zeroForOne;
swapData.isExactInput = swapData.specifiedAmount > 0;
// tick (token1Qty/token0Qty) will increase for swapping from token1 to token0
bool willUpTick = (swapData.isExactInput != isToken0);
bool willUpTick = (swapData.isExactInput != zeroForOne);
uint160 limitSqrtP = willUpTick ? TickMath.MAX_SQRT_RATIO - 1 : TickMath.MIN_SQRT_RATIO + 1;
(
swapData.baseL,
swapData.reinvestL,
@@ -235,7 +158,7 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
swapFeeUnits,
swapData.specifiedAmount,
swapData.isExactInput,
swapData.isToken0
swapData.zeroForOne
);
swapData.specifiedAmount -= usedAmount;
@@ -246,10 +169,6 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
// if price has not reached the next sqrt price (still liquidity in current tick)
if (swapData.sqrtP != swapData.nextSqrtP) {
swapData.currentTick = TickMath.getTickAtSqrtRatio(swapData.sqrtP);
if (swapData.amountsIndex == amounts.length - 1) {
break;
}
// use max since we want to over estimate
swapData.gasResidual = max(swapData.gasResidual, gasBefore - gasleft());
gasBefore = gasleft();
@@ -270,7 +189,7 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
gasBefore = gasleft();
}
if (swapData.specifiedAmount == 0) {
(result.amounts0[swapData.amountsIndex], result.amounts1[swapData.amountsIndex]) = isToken0 ==
(result.amounts0[swapData.amountsIndex], result.amounts1[swapData.amountsIndex]) = zeroForOne ==
swapData.isExactInput
? (amounts[swapData.amountsIndex], swapData.returnedAmount)
: (swapData.returnedAmount, amounts[swapData.amountsIndex]);
@@ -287,12 +206,11 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
}
for (uint256 i = swapData.amountsIndex; i < amounts.length; ++i) {
(result.amounts0[i], result.amounts1[i]) = isToken0 == swapData.isExactInput
(result.amounts0[i], result.amounts1[i]) = zeroForOne == swapData.isExactInput
? (amounts[i], swapData.returnedAmount)
: (swapData.returnedAmount, amounts[i]);
result.gasEstimates[i] = swapData.gasAggregate + swapData.gasResidual;
}
result.gasEstimates[swapData.amountsIndex] = swapData.gasAggregate + swapData.gasResidual;
}
function max(uint256 a, uint256 b) internal pure returns (uint256) {
@@ -301,7 +219,7 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
/// @dev Update liquidity net data and do cross tick
function _updateLiquidityAndCrossTick(
IPool pool,
IKyberElasticPool pool,
int24 nextTick,
uint128 currentLiquidity,
bool willUpTick
@@ -319,4 +237,14 @@ contract KyberElasticMultiQuoter is IMultiQuoter {
liquidityNet >= 0
);
}
/// @inheritdoc MultiQuoter
function pathHasMultiplePools(bytes memory path) internal pure override returns (bool) {
return path.hasMultiplePools();
}
/// @inheritdoc MultiQuoter
function pathSkipToken(bytes memory path) internal pure override returns (bytes memory) {
return path.skipToken();
}
}

View File

@@ -34,7 +34,7 @@ contract KyberElasticSampler is KyberElasticCommon {
/// @return outputAmounts Maker amounts bought at each taker token amount.
function sampleSellsFromKyberElastic(
IMultiQuoter quoter,
IFactory factory,
address factory,
address[] memory path,
uint256[] memory inputAmounts
) public view returns (bytes[] memory paths, uint256[] memory gasEstimates, uint256[] memory outputAmounts) {
@@ -42,7 +42,12 @@ contract KyberElasticSampler is KyberElasticCommon {
paths = new bytes[](inputAmounts.length);
gasEstimates = new uint256[](inputAmounts.length);
IPool[][] memory poolPaths = _getPoolPaths(quoter, factory, path, inputAmounts[inputAmounts.length - 1]);
IKyberElasticPool[][] memory poolPaths = _getPoolPaths(
quoter,
factory,
path,
inputAmounts[inputAmounts.length - 1]
);
for (uint256 i = 0; i < poolPaths.length; ++i) {
if (!_isValidPoolPath(poolPaths[i])) {
continue;
@@ -50,17 +55,30 @@ contract KyberElasticSampler is KyberElasticCommon {
bytes memory dexPath = _toPath(path, poolPaths[i]);
(uint256[] memory amountsOut, uint256[] memory gasEstimate) = quoter.quoteExactMultiInput(
factory,
dexPath,
inputAmounts
);
uint256[] memory amountsOut;
uint256[] memory gasEstimatesTemp;
for (uint256 j = 0; j < outputAmounts.length; ++j) {
if (outputAmounts[j] < amountsOut[j]) {
outputAmounts[j] = amountsOut[j];
paths[j] = dexPath;
gasEstimates[j] = gasEstimate[j];
try quoter.quoteExactMultiInput(factory, dexPath, inputAmounts) {} catch (bytes memory reason) {
bool success;
(success, amountsOut, gasEstimatesTemp) = catchKyberElasticMultiSwapResult(reason);
if (!success) {
continue;
}
for (uint256 j = 0; j < amountsOut.length; ++j) {
if (amountsOut[j] == 0) {
break;
}
if (outputAmounts[j] < amountsOut[j]) {
outputAmounts[j] = amountsOut[j];
paths[j] = dexPath;
gasEstimates[j] = gasEstimatesTemp[j];
} else if (outputAmounts[j] == amountsOut[j] && gasEstimates[j] > gasEstimatesTemp[j]) {
paths[j] = dexPath;
gasEstimates[j] = gasEstimatesTemp[j];
}
}
}
}
@@ -75,7 +93,7 @@ contract KyberElasticSampler is KyberElasticCommon {
/// @return outputAmounts Taker amounts sold at each maker token amount.
function sampleBuysFromKyberElastic(
IMultiQuoter quoter,
IFactory factory,
address factory,
address[] memory path,
uint256[] memory inputAmounts
) public view returns (bytes[] memory paths, uint256[] memory gasEstimates, uint256[] memory outputAmounts) {
@@ -84,7 +102,7 @@ contract KyberElasticSampler is KyberElasticCommon {
gasEstimates = new uint256[](inputAmounts.length);
address[] memory reversedPath = _reverseTokenPath(path);
IPool[][] memory poolPaths = _getPoolPaths(
IKyberElasticPool[][] memory poolPaths = _getPoolPaths(
quoter,
factory,
reversedPath,
@@ -98,17 +116,30 @@ contract KyberElasticSampler is KyberElasticCommon {
bytes memory poolPath = _toPath(reversedPath, poolPaths[i]);
(uint256[] memory amountsOut, uint256[] memory gasEstimate) = quoter.quoteExactMultiOutput(
factory,
poolPath,
inputAmounts
);
uint256[] memory amountsIn;
uint256[] memory gasEstimatesTemp;
for (uint256 j = 0; j < amountsOut.length; ++j) {
if (amountsOut[j] > 0 && (outputAmounts[j] == 0 || outputAmounts[j] < amountsOut[j])) {
outputAmounts[j] = amountsOut[j];
paths[j] = _toPath(path, _reversePoolPath(poolPaths[i]));
gasEstimates[j] = gasEstimate[j];
try quoter.quoteExactMultiOutput(factory, poolPath, inputAmounts) {} catch (bytes memory reason) {
bool success;
(success, amountsIn, gasEstimatesTemp) = catchKyberElasticMultiSwapResult(reason);
if (!success) {
continue;
}
for (uint256 j = 0; j < amountsIn.length; ++j) {
if (amountsIn[j] == 0) {
break;
}
if (outputAmounts[j] == 0 || outputAmounts[j] > amountsIn[j]) {
outputAmounts[j] = amountsIn[j];
paths[j] = _toPath(path, _reversePoolPath(poolPaths[i]));
gasEstimates[j] = gasEstimatesTemp[j];
} else if (outputAmounts[j] == amountsIn[j] && outputAmounts[j] > gasEstimatesTemp[j]) {
paths[j] = _toPath(path, _reversePoolPath(poolPaths[i]));
gasEstimates[j] = gasEstimatesTemp[j];
}
}
}
}

View File

@@ -0,0 +1,180 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity >=0.7 <=0.8;
pragma experimental ABIEncoderV2;
import "./interfaces/IMultiQuoter.sol";
/// @title Provides quotes for multiple swap amounts
/// @notice Allows getting the expected amount out or amount in for multiple given swap amounts without executing the swap
abstract contract MultiQuoter is IMultiQuoter {
// TODO: both quoteExactMultiInput and quoteExactMultiOutput revert at the end of the quoting logic
// and return results encodied into a revert reason. The revert should be removed and replaced with
// a normal return statement whenever the Tick Based AMM samplers stop having the two pool filtering
// logic. The two pool filtering logic causes pool's storage slots to be warmed up, causing gas estimates
// to be significantly below the gas used during settlement. Additionally, per the following EIP
// this revert logic might not clear warm storage slots in the future: https://eips.ethereum.org/EIPS/eip-3978
// the result of multiswap
struct MultiSwapResult {
// the gas estimate for each of swap amounts
uint256[] gasEstimates;
// the token0 delta for each swap amount, positive indicates sent and negative indicates receipt
int256[] amounts0;
// the token1 delta for each swap amount, positive indicates sent and negative indicates receipt
int256[] amounts1;
}
// @inheritdoc IMultiQuoter
function quoteExactMultiInput(
address factory,
bytes memory path,
uint256[] memory amountsIn
) external view override {
for (uint256 i = 0; i < amountsIn.length - 1; ++i) {
require(amountsIn[i] <= amountsIn[i + 1], "MultiQuoter/amountsIn must be monotonically increasing");
}
uint256[] memory gasEstimates = new uint256[](amountsIn.length);
while (true) {
(address pool, bool zeroForOne) = getFirstSwapDetails(factory, path, true);
// multiswap only accepts int256[] for input amounts
int256[] memory amounts = new int256[](amountsIn.length);
for (uint256 i = 0; i < amountsIn.length; ++i) {
amounts[i] = int256(amountsIn[i]);
}
MultiSwapResult memory result = multiswap(pool, zeroForOne, amounts);
for (uint256 i = 0; i < amountsIn.length; ++i) {
amountsIn[i] = zeroForOne ? uint256(-result.amounts1[i]) : uint256(-result.amounts0[i]);
gasEstimates[i] += result.gasEstimates[i];
}
// decide whether to continue or terminate
if (pathHasMultiplePools(path)) {
path = pathSkipToken(path);
} else {
// quote results must be encoded into a revert because otherwise subsequent calls
// to MultiQuoter result in multiswap hitting pool storage slots that are
// already warm. This results in very inaccurate gas estimates when estimating gas
// usage for settlement.
revertWithAmountsAndGas(amountsIn, gasEstimates);
}
}
}
// @inheritdoc IMultiQuoter
function quoteExactMultiOutput(
address factory,
bytes memory path,
uint256[] memory amountsOut
) external view override {
uint256[] memory amountsIn = new uint256[](amountsOut.length);
uint256[] memory gasEstimates = new uint256[](amountsOut.length);
for (uint256 i = 0; i < amountsOut.length - 1; ++i) {
require(amountsOut[i] <= amountsOut[i + 1], "MultiQuoter/amountsOut must be monotonically increasing");
}
uint256 nextAmountsLength = amountsOut.length;
while (true) {
(address pool, bool zeroForOne) = getFirstSwapDetails(factory, path, false);
// multiswap only accepts int256[] for output amounts
int256[] memory amounts = new int256[](nextAmountsLength);
for (uint256 i = 0; i < nextAmountsLength; ++i) {
amounts[i] = -int256(amountsOut[i]);
}
MultiSwapResult memory result = multiswap(pool, zeroForOne, amounts);
for (uint256 i = 0; i < nextAmountsLength; ++i) {
uint256 amountReceived = zeroForOne ? uint256(-result.amounts1[i]) : uint256(-result.amounts0[i]);
if (amountReceived != amountsOut[i]) {
// for exact output swaps we need to check whether we would receive the full amount due to
// multiswap behavior when hitting the limit
nextAmountsLength = i;
break;
} else {
// populate amountsOut for the next pool
amountsOut[i] = zeroForOne ? uint256(result.amounts0[i]) : uint256(result.amounts1[i]);
gasEstimates[i] += result.gasEstimates[i];
}
}
if (nextAmountsLength == 0 || !pathHasMultiplePools(path)) {
for (uint256 i = 0; i < nextAmountsLength; ++i) {
amountsIn[i] = amountsOut[i];
}
// quote results must be encoded into a revert because otherwise subsequent calls
// to MultiQuoter result in multiswap hitting pool storage slots that are
// already warm. This results in very inaccurate gas estimates when estimating gas
// usage for settlement.
revertWithAmountsAndGas(amountsIn, gasEstimates);
}
path = pathSkipToken(path);
}
}
/// @notice reverts the transaction while encoding amounts and gas estimates into the revert reason
/// @param amounts the amounts to encode as the first encoding in the revert reason
/// @param gasEstimates the gas estimates to encode as the second encoding in the revert reason
function revertWithAmountsAndGas(uint256[] memory amounts, uint256[] memory gasEstimates) private pure {
bytes memory revertResult = abi.encodeWithSignature("result(uint256[],uint256[])", amounts, gasEstimates);
assembly {
revert(add(revertResult, 0x20), mload(revertResult))
}
}
/// @notice get the details of the first swap including the pool to swap within and direction of the swap
/// @param factory The factory contract managing the tick based AMM pools
/// @param path The path of the swap as a combination of token pairs and/or pool fee
/// @param isExactInput whether or not this wap is an exact input or exact output swap (this determines whether tokenIn is first or second in the path)
/// @return pool The first pool derived from the path.
/// @return zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0
function getFirstSwapDetails(
address factory,
bytes memory path,
bool isExactInput
) internal view virtual returns (address pool, bool zeroForOne);
/// @notice swap multiple amounts of token0 for token1 or token1 for token1
/// @dev The results of multiswap includes slight rounding issues resulting from rounding up/rounding down in SqrtPriceMath library. Additionally,
/// it should be noted that multiswap requires a monotonically increasing list of amounts for exact inputs and monotonically decreasing list of
/// amounts for exact outputs.
/// @param pool The tick based AMM pool to simulate each of the swap amounts for
/// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0
/// @param amounts The amounts of the swaps, positive values indicate exactInput and negative values indicate exact output
/// @return result The results of the swap as a MultiSwapResult struct with gas used, token0 and token1 deltas
function multiswap(
address pool,
bool zeroForOne,
int256[] memory amounts
) internal view virtual returns (MultiSwapResult memory result);
/// @notice Returns true iff the path contains two or more pools
/// @param path The encoded swap path
/// @return True if path contains two or more pools, otherwise false
function pathHasMultiplePools(bytes memory path) internal pure virtual returns (bool);
/// @notice Skips a token and/or fee element from the path and returns the remainder
/// @param path The swap path
/// @return The remaining token + fee elements in the path
function pathSkipToken(bytes memory path) internal pure virtual returns (bytes memory);
}

View File

@@ -23,6 +23,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./interfaces/IUniswapV3.sol";
import "./interfaces/IMultiQuoter.sol";
contract UniswapV3Common {
/// @dev Gas limit for UniswapV3 calls
@@ -79,8 +80,8 @@ contract UniswapV3Common {
/// @dev Returns `poolPaths` to sample against. The caller is responsible for not using path involinvg zero address(es).
function getPoolPaths(
IUniswapV3Factory factory,
IUniswapV3MultiQuoter multiQuoter,
address factory,
IMultiQuoter multiQuoter,
IERC20TokenV06[] memory path,
uint256 inputAmount
) internal view returns (IUniswapV3Pool[][] memory poolPaths) {
@@ -94,8 +95,8 @@ contract UniswapV3Common {
}
function getPoolPathSingleHop(
IUniswapV3Factory factory,
IUniswapV3MultiQuoter multiQuoter,
address factory,
IMultiQuoter multiQuoter,
IERC20TokenV06[] memory path,
uint256 inputAmount
) private view returns (IUniswapV3Pool[][] memory poolPaths) {
@@ -120,8 +121,8 @@ contract UniswapV3Common {
}
function getPoolPathTwoHop(
IUniswapV3Factory factory,
IUniswapV3MultiQuoter multiQuoter,
address factory,
IMultiQuoter multiQuoter,
IERC20TokenV06[] memory path,
uint256 inputAmount
) private view returns (IUniswapV3Pool[][] memory poolPaths) {
@@ -159,8 +160,8 @@ contract UniswapV3Common {
}
struct GetTopTwoPoolsParams {
IUniswapV3Factory factory;
IUniswapV3MultiQuoter multiQuoter;
address factory;
IMultiQuoter multiQuoter;
IERC20TokenV06 inputToken;
IERC20TokenV06 outputToken;
uint256 inputAmount;
@@ -180,10 +181,12 @@ contract UniswapV3Common {
uint24[4] memory validPoolFees = [uint24(0.0001e6), uint24(0.0005e6), uint24(0.003e6), uint24(0.01e6)];
for (uint256 i = 0; i < validPoolFees.length; ++i) {
IUniswapV3Pool pool = params.factory.getPool(
address(params.inputToken),
address(params.outputToken),
validPoolFees[i]
IUniswapV3Pool pool = IUniswapV3Pool(
IUniswapV3Factory(params.factory).getPool(
address(params.inputToken),
address(params.outputToken),
validPoolFees[i]
)
);
if (!isValidPool(pool)) {
continue;
@@ -200,7 +203,7 @@ contract UniswapV3Common {
inputAmounts
)
{} catch (bytes memory reason) {
(bool success, uint256[] memory outputAmounts, ) = catchMultiSwapResult(reason);
(bool success, uint256[] memory outputAmounts, ) = catchUniswapV3MultiSwapResult(reason);
if (success) {
// Keeping track of the top 2 pools.
if (outputAmounts[0] > topOutputAmounts[0]) {
@@ -247,7 +250,7 @@ contract UniswapV3Common {
return true;
}
function catchMultiSwapResult(
function catchUniswapV3MultiSwapResult(
bytes memory revertReason
) internal pure returns (bool success, uint256[] memory amounts, uint256[] memory gasEstimates) {
bytes4 selector;

View File

@@ -24,16 +24,9 @@ import "@uniswap/v3-core/contracts/libraries/SafeCast.sol";
import "@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol";
import "./interfaces/IUniswapV3.sol";
import "./MultiQuoter.sol";
/// @title Provides quotes for multiple swap amounts
/// @notice Allows getting the expected amount out or amount in for multiple given swap amounts without executing the swap
contract UniswapV3MultiQuoter is IUniswapV3MultiQuoter {
// TODO: both quoteExactMultiInput and quoteExactMultiOutput revert at the end of the quoting logic
// and return results encodied into a revert reason. The revert should be removed and replaced with
// a normal return statement whenever UniswapV3Sampler stops having the two pool filtering logic.
// The two pool filtering logic causes pool's storage slots to be warmed up, causing gas estimates
// to be significantly below the gas used during settlement. Additionally, per the following EIP
// this revert logic might not clear warm storage slots in the future: https://eips.ethereum.org/EIPS/eip-3978
contract UniswapV3MultiQuoter is MultiQuoter {
using Path for bytes;
using SafeCast for uint256;
using LowGasSafeMath for int256;
@@ -76,175 +69,38 @@ contract UniswapV3MultiQuoter is IUniswapV3MultiQuoter {
uint256 gasBefore;
}
// the result of multiswap
struct MultiSwapResult {
// the gas estimate for each of swap amounts
uint256[] gasEstimates;
// the token0 delta for each swap amount, positive indicates sent and negative indicates receipt
int256[] amounts0;
// the token1 delta for each swap amount, positive indicates sent and negative indicates receipt
int256[] amounts1;
}
/// @notice Quotes amounts out for a set of exact input swaps and provides results encoded into a revert reason
/// @param factory The factory contract managing UniswapV3 pools
/// @param path The path of the swap, i.e. each token pair and the pool fee
/// @param amountsIn The amounts in of the first token to swap
/// @dev This function reverts at the end of the quoting logic and encodes (uint256[] amountsOut, uint256[] gasEstimates)
/// into the revert reason. See additional documentation below.
function quoteExactMultiInput(
IUniswapV3Factory factory,
/// @inheritdoc MultiQuoter
function getFirstSwapDetails(
address factory,
bytes memory path,
uint256[] memory amountsIn
) public view override {
for (uint256 i = 0; i < amountsIn.length - 1; ++i) {
require(
amountsIn[i] <= amountsIn[i + 1],
"UniswapV3MultiQuoter/amountsIn must be monotonically increasing"
);
bool isExactInput
) internal view override returns (address pool, bool zeroForOne) {
address tokenIn;
address tokenOut;
uint24 fee;
if (isExactInput) {
(tokenIn, tokenOut, fee) = path.decodeFirstPool();
} else {
(tokenOut, tokenIn, fee) = path.decodeFirstPool();
}
uint256[] memory gasEstimates = new uint256[](amountsIn.length);
while (true) {
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
bool zeroForOne = tokenIn < tokenOut;
IUniswapV3Pool pool = factory.getPool(tokenIn, tokenOut, fee);
// multiswap only accepts int256[] for input amounts
int256[] memory amounts = new int256[](amountsIn.length);
for (uint256 i = 0; i < amountsIn.length; ++i) {
amounts[i] = int256(amountsIn[i]);
}
MultiSwapResult memory result = multiswap(
pool,
zeroForOne,
amounts,
zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1
);
for (uint256 i = 0; i < amountsIn.length; ++i) {
amountsIn[i] = zeroForOne ? uint256(-result.amounts1[i]) : uint256(-result.amounts0[i]);
gasEstimates[i] += result.gasEstimates[i];
}
// decide whether to continue or terminate
if (path.hasMultiplePools()) {
path = path.skipToken();
} else {
// quote results must be encoded into a revert because otherwise subsequent calls
// to UniswapV3MultiQuoter result in multiswap hitting pool storage slots that are
// already warm. This results in very inaccurate gas estimates when estimating gas
// usage for settlement.
bytes memory revertResult = abi.encodeWithSignature(
"result(uint256[],uint256[])",
amountsIn,
gasEstimates
);
assembly {
revert(add(revertResult, 0x20), mload(revertResult))
}
}
}
zeroForOne = tokenIn < tokenOut;
pool = IUniswapV3Factory(factory).getPool(tokenIn, tokenOut, fee);
}
/// @notice Quotes amounts in a set of exact output swaps and provides results encoded into a revert reason
/// @param factory The factory contract managing UniswapV3 pools
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
/// @param amountsOut The amounts out of the last token to receive
/// @dev This function reverts at the end of the quoting logic and encodes (uint256[] amountsIn, uint256[] gasEstimates)
/// into the revert reason. See additional documentation below.
function quoteExactMultiOutput(
IUniswapV3Factory factory,
bytes memory path,
uint256[] memory amountsOut
) public view override {
uint256[] memory amountsIn = new uint256[](amountsOut.length);
uint256[] memory gasEstimates = new uint256[](amountsOut.length);
for (uint256 i = 0; i < amountsOut.length - 1; ++i) {
require(
amountsOut[i] <= amountsOut[i + 1],
"UniswapV3MultiQuoter/amountsOut must be monotonically increasing"
);
}
uint256 nextAmountsLength = amountsOut.length;
while (true) {
(address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool();
bool zeroForOne = tokenIn < tokenOut;
IUniswapV3Pool pool = factory.getPool(tokenIn, tokenOut, fee);
// multiswap only accepts int256[] for output amounts
int256[] memory amounts = new int256[](nextAmountsLength);
for (uint256 i = 0; i < nextAmountsLength; ++i) {
amounts[i] = -int256(amountsOut[i]);
}
MultiSwapResult memory result = multiswap(
pool,
zeroForOne,
amounts,
zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1
);
for (uint256 i = 0; i < nextAmountsLength; ++i) {
uint256 amountReceived = zeroForOne ? uint256(-result.amounts1[i]) : uint256(-result.amounts0[i]);
if (amountReceived != amountsOut[i]) {
// for exact output swaps we need to check whether we would receive the full amount due to
// multiswap behavior when hitting the limit
nextAmountsLength = i;
break;
} else {
// populate amountsOut for the next pool
amountsOut[i] = zeroForOne ? uint256(result.amounts0[i]) : uint256(result.amounts1[i]);
gasEstimates[i] += result.gasEstimates[i];
}
}
if (nextAmountsLength == 0 || !path.hasMultiplePools()) {
for (uint256 i = 0; i < nextAmountsLength; ++i) {
amountsIn[i] = amountsOut[i];
}
// quote results must be encoded into a revert because otherwise subsequent calls
// to UniswapV3MultiQuoter result in multiswap hitting pool storage slots that are
// already warm. This results in very inaccurate gas estimates when estimating gas
// usage for settlement.
bytes memory revertResult = abi.encodeWithSignature(
"result(uint256[],uint256[])",
amountsIn,
gasEstimates
);
assembly {
revert(add(revertResult, 0x20), mload(revertResult))
}
}
path = path.skipToken();
}
}
/// @notice swap multiple amounts of token0 for token1 or token1 for token1
/// @dev The results of multiswap includes slight rounding issues resulting from rounding up/rounding down in SqrtPriceMath library. Additionally,
/// it should be noted that multiswap requires a monotonically increasing list of amounts for exact inputs and monotonically decreasing list of
/// amounts for exact outputs.
/// @param pool The UniswapV3 pool to simulate each of the swap amounts for
/// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0
/// @param amounts The amounts of the swaps, positive values indicate exactInput and negative values indicate exact output
/// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this
/// value after the swap. If one for zero, the price cannot be greater than this value after the swap
/// @return result The results of the swap as a MultiSwapResult struct with gas used, token0 and token1 deltas
/// @inheritdoc MultiQuoter
function multiswap(
IUniswapV3Pool pool,
address p,
bool zeroForOne,
int256[] memory amounts,
uint160 sqrtPriceLimitX96
) private view returns (MultiSwapResult memory result) {
int256[] memory amounts
) internal view override returns (MultiSwapResult memory result) {
result.gasEstimates = new uint256[](amounts.length);
result.amounts0 = new int256[](amounts.length);
result.amounts1 = new int256[](amounts.length);
uint160 sqrtPriceLimitX96 = zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1;
IUniswapV3Pool pool = IUniswapV3Pool(p);
(uint160 sqrtPriceX96Start, int24 tickStart, , , , , ) = pool.slot0();
int24 tickSpacing = pool.tickSpacing();
uint24 fee = pool.fee();
@@ -377,7 +233,7 @@ contract UniswapV3MultiQuoter is IUniswapV3MultiQuoter {
int24 tick,
int24 tickSpacing,
bool lte
) private view returns (int24 next, bool initialized) {
) internal view returns (int24 next, bool initialized) {
int24 compressed = tick / tickSpacing;
if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity
@@ -417,4 +273,14 @@ contract UniswapV3MultiQuoter is IUniswapV3MultiQuoter {
wordPos = int16(tick >> 8);
bitPos = uint8(tick % 256);
}
/// @inheritdoc MultiQuoter
function pathHasMultiplePools(bytes memory path) internal pure override returns (bool) {
return path.hasMultiplePools();
}
/// @inheritdoc MultiQuoter
function pathSkipToken(bytes memory path) internal pure override returns (bytes memory) {
return path.skipToken();
}
}

View File

@@ -23,11 +23,10 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./UniswapV3Common.sol";
import "./interfaces/IUniswapV3.sol";
import "./interfaces/IMultiQuoter.sol";
contract UniswapV3Sampler is UniswapV3Common {
IUniswapV3MultiQuoter private constant multiQuoter =
IUniswapV3MultiQuoter(0x5555555555555555555555555555555555555556);
IMultiQuoter private constant multiQuoter = IMultiQuoter(0x5555555555555555555555555555555555555556);
/// @dev Sample sell quotes from UniswapV3.
/// @param factory UniswapV3 Factory contract.
@@ -37,7 +36,7 @@ contract UniswapV3Sampler is UniswapV3Common {
/// @return uniswapGasUsed Estimated amount of gas used
/// @return makerTokenAmounts Maker amounts bought at each taker token amount.
function sampleSellsFromUniswapV3(
IUniswapV3Factory factory,
address factory,
IERC20TokenV06[] memory path,
uint256[] memory takerTokenAmounts
)
@@ -69,7 +68,7 @@ contract UniswapV3Sampler is UniswapV3Common {
bytes memory reason
) {
bool success;
(success, amountsOut, gasEstimates) = catchMultiSwapResult(reason);
(success, amountsOut, gasEstimates) = catchUniswapV3MultiSwapResult(reason);
if (!success) {
continue;
@@ -101,7 +100,7 @@ contract UniswapV3Sampler is UniswapV3Common {
/// @return uniswapGasUsed Estimated amount of gas used
/// @return takerTokenAmounts Taker amounts sold at each maker token amount.
function sampleBuysFromUniswapV3(
IUniswapV3Factory factory,
address factory,
IERC20TokenV06[] memory path,
uint256[] memory makerTokenAmounts
)
@@ -134,7 +133,7 @@ contract UniswapV3Sampler is UniswapV3Common {
bytes memory reason
) {
bool success;
(success, amountsIn, gasEstimates) = catchMultiSwapResult(reason);
(success, amountsIn, gasEstimates) = catchUniswapV3MultiSwapResult(reason);
if (!success) {
continue;

View File

@@ -1,43 +1,5 @@
pragma solidity >=0.6;
interface IAlgebraQuoter {
/// @notice Returns the amount out received for a given exact input swap without executing the swap
/// @param path The path of the swap, i.e. each token pair
/// @param amountIn The amount of the first token to swap
/// @return amountOut The amount of the last token that would be received
function quoteExactInput(
bytes memory path,
uint256 amountIn
) external returns (uint256 amountOut, uint16[] memory fees);
/// @notice Returns the amount in required for a given exact output swap without executing the swap
/// @param path The path of the swap, i.e. each token pair. Path must be provided in reverse order
/// @param amountOut The amount of the last token to receive
/// @return amountIn The amount of first token required to be paid
function quoteExactOutput(
bytes memory path,
uint256 amountOut
) external returns (uint256 amountIn, uint16[] memory fees);
}
interface IAlgebraMultiQuoter {
// @notice Returns the amounts out received for a given set of exact input swaps without executing the swap
/// @param factory The factory contract managing UniswapV3 pools
/// @param path The path of the swap, i.e. each token pair and the pool fee
/// @param amountsIn The amounts in of the first token to swap
function quoteExactMultiInput(IAlgebraFactory factory, bytes memory path, uint256[] memory amountsIn) external view;
/// @notice Returns the amounts in received for a given set of exact output swaps without executing the swap
/// @param factory The factory contract managing UniswapV3 pools
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
/// @param amountsOut The amounts out of the last token to receive
function quoteExactMultiOutput(
IAlgebraFactory factory,
bytes memory path,
uint256[] memory amountsOut
) external view;
}
interface IAlgebraFactory {
/**
* @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist
@@ -46,7 +8,7 @@ interface IAlgebraFactory {
* @param tokenB The contract address of the other token
* @return pool The pool address
*/
function poolByPair(address tokenA, address tokenB) external view returns (IAlgebraPool pool);
function poolByPair(address tokenA, address tokenB) external view returns (address pool);
}
interface IAlgebraPool {

View File

@@ -1,7 +1,7 @@
pragma solidity >=0.6;
pragma experimental ABIEncoderV2;
interface IFactory {
interface IKyberElasticFactory {
/// @notice Returns the pool address for a given pair of tokens and a swap fee
/// @dev Token order does not matter
/// @param tokenA Contract address of either token0 or token1
@@ -9,14 +9,9 @@ interface IFactory {
/// @param swapFeeUnits Fee to be collected upon every swap in the pool, in fee units
/// @return pool The pool address. Returns null address if it does not exist
function getPool(address tokenA, address tokenB, uint24 swapFeeUnits) external view returns (address pool);
function parameters()
external
view
returns (address factory, address token0, address token1, uint24 swapFeeUnits, int24 tickDistance);
}
interface IPool {
interface IKyberElasticPool {
function token0() external view returns (address);
function token1() external view returns (address);
@@ -25,19 +20,6 @@ interface IPool {
/// @return The swap fee in basis points
function swapFeeUnits() external view returns (uint24);
/// @notice The pool tick distance
/// @dev Ticks can only be initialized and used at multiples of this value
/// It remains an int24 to avoid casting even though it is >= 1.
/// e.g: a tickDistance of 5 means ticks can be initialized every 5th tick, i.e., ..., -10, -5, 0, 5, 10, ...
/// @return The tick distance
function tickDistance() external view returns (int24);
/// @notice Maximum gross liquidity that an initialized tick can have
/// @dev This is to prevent overflow the pool's active base liquidity (uint128)
/// also prevents out-of-range liquidity from being used to prevent adding in-range liquidity to a pool
/// @return The max amount of liquidity per tick
function maxTickLiquidity() external view returns (uint128);
/// @notice Look up information about a specific tick in the pool
/// @param tick The tick to look up
/// @return liquidityGross total liquidity amount from positions that uses this tick as a lower or upper tick
@@ -61,15 +43,6 @@ interface IPool {
/// @param tick The tick to look up
function initializedTicks(int24 tick) external view returns (int24 previous, int24 next);
/// @notice Returns the information about a position by the position's key
/// @return liquidity the liquidity quantity of the position
/// @return feeGrowthInsideLast fee growth inside the tick range as of the last mint / burn action performed
function getPositions(
address owner,
int24 tickLower,
int24 tickUpper
) external view returns (uint128 liquidity, uint256 feeGrowthInsideLast);
/// @notice Fetches the pool's prices, ticks and lock status
/// @return sqrtP sqrt of current price: sqrt(token1/token0)
/// @return currentTick pool's current tick
@@ -86,90 +59,3 @@ interface IPool {
/// @return reinvestLLast last cached value of reinvestL, used for calculating reinvestment token qty
function getLiquidityState() external view returns (uint128 baseL, uint128 reinvestL, uint128 reinvestLLast);
}
/// @title QuoterV2 Interface
/// @notice Supports quoting the calculated amounts from exact input or exact output swaps.
/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap.
/// @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 IQuoterV2 {
struct QuoteOutput {
uint256 usedAmount;
uint256 returnedAmount;
uint160 afterSqrtP;
uint32 initializedTicksCrossed;
uint256 gasEstimate;
}
/// @notice Returns the amount out received for a given exact input swap without executing the swap
/// @param path The path of the swap, i.e. each token pair and the pool fee
/// @param amountIn The amount of the first token to swap
/// @return amountOut The amount of the last token that would be received
/// @return afterSqrtPList List of the sqrt price after the swap for each pool in the path
/// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
/// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactInput(
bytes memory path,
uint256 amountIn
)
external
returns (
uint256 amountOut,
uint160[] memory afterSqrtPList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
struct QuoteExactInputSingleParams {
address tokenIn;
address tokenOut;
uint256 amountIn;
uint24 feeUnits;
uint160 limitSqrtP;
}
/// @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`
/// tokenIn The token being swapped in
/// tokenOut The token being swapped out
/// fee The fee of the token pool to consider for the pair
/// amountIn The desired input amount
/// limitSqrtP The price limit of the pool that cannot be exceeded by the swap
function quoteExactInputSingle(QuoteExactInputSingleParams memory params) external returns (QuoteOutput memory);
/// @notice Returns the amount in required for a given exact output swap without executing the swap
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
/// @param amountOut The amount of the last token to receive
/// @return amountIn The amount of first token required to be paid
/// @return afterSqrtPList List of the sqrt price after the swap for each pool in the path
/// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
/// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactOutput(
bytes memory path,
uint256 amountOut
)
external
returns (
uint256 amountIn,
uint160[] memory afterSqrtPList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
struct QuoteExactOutputSingleParams {
address tokenIn;
address tokenOut;
uint256 amount;
uint24 feeUnits;
uint160 limitSqrtP;
}
/// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool
/// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams`
/// tokenIn The token being swapped in
/// tokenOut The token being swapped out
/// fee The fee of the token pool to consider for the pair
/// amountOut The desired output amount
/// limitSqrtP The price limit of the pool that cannot be exceeded by the swap
function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params) external returns (QuoteOutput memory);
}

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2023 ZeroEx Intl.
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,18 +19,20 @@
pragma solidity >=0.6;
import "./IKyberElastic.sol";
interface IMultiQuoter {
function quoteExactMultiInput(
IFactory factory,
bytes memory path,
uint256[] memory amountsIn
) external view returns (uint256[] memory amountsOut, uint256[] memory gasEstimate);
/// @notice Quotes amounts out for a set of exact input swaps and provides results encoded into a revert reason
/// @param factory The factory contract managing the tick based AMM pools
/// @param path The path of the swap, i.e. each token pair and the pool fee
/// @param amountsIn The amounts in of the first token to swap
/// @dev This function reverts at the end of the quoting logic and encodes (uint256[] amountsOut, uint256[] gasEstimates)
/// into the revert reason. See additional documentation below.
function quoteExactMultiInput(address factory, bytes memory path, uint256[] memory amountsIn) external view;
function quoteExactMultiOutput(
IFactory factory,
bytes memory path,
uint256[] memory amountsOut
) external view returns (uint256[] memory amountsIn, uint256[] memory gasEstimate);
/// @notice Quotes amounts in a set of exact output swaps and provides results encoded into a revert reason
/// @param factory The factory contract managing the tick based AMM pools
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
/// @param amountsOut The amounts out of the last token to receive
/// @dev This function reverts at the end of the quoting logic and encodes (uint256[] amountsIn, uint256[] gasEstimates)
/// into the revert reason. See additional documentation below.
function quoteExactMultiOutput(address factory, bytes memory path, uint256[] memory amountsOut) external view;
}

View File

@@ -1,48 +1,5 @@
pragma solidity >=0.6;
interface IUniswapV3QuoterV2 {
/// @return Returns the address of the Uniswap V3 factory
function factory() external view returns (IUniswapV3Factory);
// @notice Returns the amount out received for a given exact input swap without executing the swap
// @param path The path of the swap, i.e. each token pair and the pool fee
// @param amountIn The amount of the first token to swap
// @return amountOut The amount of the last token that would be received
// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactInput(
bytes memory path,
uint256 amountIn
)
external
returns (
uint256 amountOut,
uint160[] memory sqrtPriceX96AfterList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
// @notice Returns the amount in required for a given exact output swap without executing the swap
// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
// @param amountOut The amount of the last token to receive
// @return amountIn The amount of first token required to be paid
// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactOutput(
bytes memory path,
uint256 amountOut
)
external
returns (
uint256 amountIn,
uint160[] memory sqrtPriceX96AfterList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
}
interface IUniswapV3Pool {
/// @notice The first of the two tokens of the pool, sorted by address
/// @return The token contract address
@@ -134,27 +91,5 @@ interface IUniswapV3Factory {
/// @param b The contract address of the other token
/// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
/// @return pool The pool address
function getPool(address a, address b, uint24 fee) external view returns (IUniswapV3Pool pool);
}
interface IUniswapV3MultiQuoter {
// @notice Returns the amounts out received for a given set of exact input swaps without executing the swap
/// @param factory The factory contract managing UniswapV3 pools
/// @param path The path of the swap, i.e. each token pair and the pool fee
/// @param amountsIn The amounts in of the first token to swap
function quoteExactMultiInput(
IUniswapV3Factory factory,
bytes memory path,
uint256[] memory amountsIn
) external view;
/// @notice Returns the amounts in received for a given set of exact output swaps without executing the swap
/// @param factory The factory contract managing UniswapV3 pools
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
/// @param amountsOut The amounts out of the last token to receive
function quoteExactMultiOutput(
IUniswapV3Factory factory,
bytes memory path,
uint256[] memory amountsOut
) external view;
function getPool(address a, address b, uint24 fee) external view returns (address pool);
}

View File

@@ -1,27 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2023 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity >=0.6;
library Multiswap {
// the result of multiswap
struct Result {
// the gas estimate for each of swap amounts
uint256[] gasEstimates;
// the token0 delta for each swap amount, positive indicates sent and negative indicates receipt
int256[] amounts0;
// the token1 delta for each swap amount, positive indicates sent and negative indicates receipt
int256[] amounts1;
}
}

View File

@@ -5,6 +5,26 @@ import "forge-std/Test.sol";
import "../src/AlgebraMultiQuoter.sol";
import "../src/AlgebraCommon.sol";
interface IAlgebraQuoter {
/// @notice Returns the amount out received for a given exact input swap without executing the swap
/// @param path The path of the swap, i.e. each token pair
/// @param amountIn The amount of the first token to swap
/// @return amountOut The amount of the last token that would be received
function quoteExactInput(
bytes memory path,
uint256 amountIn
) external returns (uint256 amountOut, uint16[] memory fees);
/// @notice Returns the amount in required for a given exact output swap without executing the swap
/// @param path The path of the swap, i.e. each token pair. Path must be provided in reverse order
/// @param amountOut The amount of the last token to receive
/// @return amountIn The amount of first token required to be paid
function quoteExactOutput(
bytes memory path,
uint256 amountOut
) external returns (uint256 amountIn, uint16[] memory fees);
}
contract TestAlgebraMultiQuoter is Test, AlgebraCommon {
/// @dev error threshold for comparison between MultiQuoter and UniswapV3's official QuoterV2.
/// MultiQuoter results in some rounding errors due to SqrtPriceMath library.
@@ -134,7 +154,7 @@ contract TestAlgebraMultiQuoter is Test, AlgebraCommon {
) private returns (uint256 algebraQuoterGasUsage, uint256 multiQuoterGasUsage) {
uint256 gas0 = gasleft();
uint256[] memory multiQuoterAmountsOut;
try multiQuoter.quoteExactMultiInput(factory, path, amountsIn) {} catch (bytes memory reason) {
try multiQuoter.quoteExactMultiInput(address(factory), path, amountsIn) {} catch (bytes memory reason) {
(, multiQuoterAmountsOut, ) = catchAlgebraMultiSwapResult(reason);
}
uint256 gas1 = gasleft();
@@ -162,7 +182,7 @@ contract TestAlgebraMultiQuoter is Test, AlgebraCommon {
) private returns (uint256 algebraQuoterGasUsage, uint256 multiQuoterGasUsage) {
uint256 gas0 = gasleft();
uint256[] memory multiQuoterAmountsIn;
try multiQuoter.quoteExactMultiOutput(factory, path, amountsOut) {} catch (bytes memory reason) {
try multiQuoter.quoteExactMultiOutput(address(factory), path, amountsOut) {} catch (bytes memory reason) {
(, multiQuoterAmountsIn, ) = catchAlgebraMultiSwapResult(reason);
}
uint256 gas1 = gasleft();

View File

@@ -4,7 +4,52 @@ pragma experimental ABIEncoderV2;
import "forge-std/Test.sol";
import "../src/KyberElasticMultiQuoter.sol";
import "../src/KyberElasticCommon.sol";
import {IPool, IFactory} from "../src/interfaces/IKyberElastic.sol";
import {IKyberElasticPool, IKyberElasticFactory} from "../src/interfaces/IKyberElastic.sol";
/// @title QuoterV2 Interface
/// @notice Supports quoting the calculated amounts from exact input or exact output swaps.
/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap.
/// @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 IQuoterV2 {
/// @notice Returns the amount out received for a given exact input swap without executing the swap
/// @param path The path of the swap, i.e. each token pair and the pool fee
/// @param amountIn The amount of the first token to swap
/// @return amountOut The amount of the last token that would be received
/// @return afterSqrtPList List of the sqrt price after the swap for each pool in the path
/// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
/// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactInput(
bytes memory path,
uint256 amountIn
)
external
returns (
uint256 amountOut,
uint160[] memory afterSqrtPList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
/// @notice Returns the amount in required for a given exact output swap without executing the swap
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
/// @param amountOut The amount of the last token to receive
/// @return amountIn The amount of first token required to be paid
/// @return afterSqrtPList List of the sqrt price after the swap for each pool in the path
/// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
/// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactOutput(
bytes memory path,
uint256 amountOut
)
external
returns (
uint256 amountIn,
uint160[] memory afterSqrtPList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
}
contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
// NOTE: Example test command: forge test --fork-url $ETH_RPC_URL --fork-block-number 16400073 --etherscan-api-key $ETHERSCAN_API_KEY --match-contract "KyberElastic"
@@ -15,11 +60,11 @@ contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
address constant ETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant LINK = 0x514910771AF9Ca656af840dff83E8264EcF986CA;
IPool constant ETH_KNC_POOL_100_BIP = IPool(0xB5e643250FF59311071C5008f722488543DD7b3C);
IPool constant ETH_KNC_POOL_30_BIP = IPool(0xa38a0165e82B7a5E8650109E9e54087a34C93020);
IPool constant LINK_ETH_POOL_30_BIP = IPool(0x8990b58Ab653C9954415f4544e8deB72c2b12ED8);
IKyberElasticPool constant ETH_KNC_POOL_100_BIP = IKyberElasticPool(0xB5e643250FF59311071C5008f722488543DD7b3C);
IKyberElasticPool constant ETH_KNC_POOL_30_BIP = IKyberElasticPool(0xa38a0165e82B7a5E8650109E9e54087a34C93020);
IKyberElasticPool constant LINK_ETH_POOL_30_BIP = IKyberElasticPool(0x8990b58Ab653C9954415f4544e8deB72c2b12ED8);
IQuoterV2 constant kyberQuoter = IQuoterV2(0x0D125c15D54cA1F8a813C74A81aEe34ebB508C1f);
IFactory constant factory = IFactory(0x5F1dddbf348aC2fbe22a163e30F99F9ECE3DD50a);
IKyberElasticFactory constant factory = IKyberElasticFactory(0x5F1dddbf348aC2fbe22a163e30F99F9ECE3DD50a);
KyberElasticMultiQuoter multiQuoter;
uint256[][] testAmounts;
@@ -61,7 +106,7 @@ contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
tokenPath[0] = ETH;
tokenPath[1] = KNC;
IPool[] memory poolPath = new IPool[](1);
IKyberElasticPool[] memory poolPath = new IKyberElasticPool[](1);
poolPath[0] = ETH_KNC_POOL_100_BIP;
testAllAmountsAndPaths(tokenPath, poolPath);
@@ -72,7 +117,7 @@ contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
tokenPath[0] = ETH;
tokenPath[1] = KNC;
IPool[] memory poolPath = new IPool[](1);
IKyberElasticPool[] memory poolPath = new IKyberElasticPool[](1);
poolPath[0] = ETH_KNC_POOL_30_BIP;
testAllAmountsAndPaths(tokenPath, poolPath);
@@ -84,7 +129,7 @@ contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
tokenPath[1] = KNC;
tokenPath[2] = LINK;
IPool[] memory poolPath = new IPool[](2);
IKyberElasticPool[] memory poolPath = new IKyberElasticPool[](2);
poolPath[0] = ETH_KNC_POOL_100_BIP;
poolPath[1] = LINK_ETH_POOL_30_BIP;
@@ -102,7 +147,7 @@ contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
tokenPath[0] = ETH;
tokenPath[1] = KNC;
IPool[][] memory poolPaths = _getPoolPaths(multiQuoter, factory, tokenPath, 1 ether);
IKyberElasticPool[][] memory poolPaths = _getPoolPaths(multiQuoter, address(factory), tokenPath, 1 ether);
assert(poolPaths.length == 2);
for (uint256 i; i < poolPaths.length; ++i) {
for (uint256 j; j < poolPaths[i].length; ++j) {
@@ -114,7 +159,7 @@ contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
}
}
function testAllAmountsAndPaths(address[] memory tokenPath, IPool[] memory poolPath) private {
function testAllAmountsAndPaths(address[] memory tokenPath, IKyberElasticPool[] memory poolPath) private {
uint256 kyberQuoterGasUsage;
uint256 multiQuoterGasUsage;
@@ -164,7 +209,10 @@ contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
uint256[] memory amountsIn
) private returns (uint256 kyberQuoterGasUsage, uint256 multiQuoterGasUsage) {
uint256 gas0 = gasleft();
(uint256[] memory multiQuoterAmountsOut, ) = multiQuoter.quoteExactMultiInput(factory, path, amountsIn);
uint256[] memory multiQuoterAmountsOut;
try multiQuoter.quoteExactMultiInput(address(factory), path, amountsIn) {} catch (bytes memory reason) {
(, multiQuoterAmountsOut, ) = catchKyberElasticMultiSwapResult(reason);
}
uint256 gas1 = gasleft();
for (uint256 i = 0; i < amountsIn.length; ++i) {
@@ -185,7 +233,10 @@ contract TestKyberElasticMultiQuoter is Test, KyberElasticCommon {
uint256[] memory amountsOut
) private returns (uint256 kyberQuoterGasUsage, uint256 multiQuoterGasUsage) {
uint256 gas0 = gasleft();
(uint256[] memory multiQuoterAmountsIn, ) = multiQuoter.quoteExactMultiOutput(factory, path, amountsOut);
uint256[] memory multiQuoterAmountsIn;
try multiQuoter.quoteExactMultiOutput(address(factory), path, amountsOut) {} catch (bytes memory reason) {
(, multiQuoterAmountsIn, ) = catchKyberElasticMultiSwapResult(reason);
}
uint256 gas1 = gasleft();
for (uint256 i = 0; i < amountsOut.length; ++i) {

View File

@@ -7,7 +7,50 @@ import "forge-std/Test.sol";
import "../src/UniswapV3MultiQuoter.sol";
import "../src/UniswapV3Common.sol";
contract TestUniswapV3Sampler is Test, UniswapV3Common {
interface IUniswapV3QuoterV2 {
/// @return Returns the address of the Uniswap V3 factory
function factory() external view returns (IUniswapV3Factory);
// @notice Returns the amount out received for a given exact input swap without executing the swap
// @param path The path of the swap, i.e. each token pair and the pool fee
// @param amountIn The amount of the first token to swap
// @return amountOut The amount of the last token that would be received
// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactInput(
bytes memory path,
uint256 amountIn
)
external
returns (
uint256 amountOut,
uint160[] memory sqrtPriceX96AfterList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
// @notice Returns the amount in required for a given exact output swap without executing the swap
// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
// @param amountOut The amount of the last token to receive
// @return amountIn The amount of first token required to be paid
// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactOutput(
bytes memory path,
uint256 amountOut
)
external
returns (
uint256 amountIn,
uint160[] memory sqrtPriceX96AfterList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
}
contract TestUniswapV3MultiQuoter is Test, UniswapV3Common {
/// @dev error threshold in wei for comparison between MultiQuoter and UniswapV3's official QuoterV2.
/// MultiQuoter results in some rounding errors due to SqrtPriceMath library.
uint256 constant ERROR_THRESHOLD = 125;
@@ -148,8 +191,8 @@ contract TestUniswapV3Sampler is Test, UniswapV3Common {
) private returns (uint256 uniQuoterGasUsage, uint256 multiQuoterGasUsage) {
uint256 gas0 = gasleft();
uint256[] memory multiQuoterAmountsOut;
try multiQuoter.quoteExactMultiInput(factory, path, amountsIn) {} catch (bytes memory reason) {
(, multiQuoterAmountsOut, ) = catchMultiSwapResult(reason);
try multiQuoter.quoteExactMultiInput(address(factory), path, amountsIn) {} catch (bytes memory reason) {
(, multiQuoterAmountsOut, ) = catchUniswapV3MultiSwapResult(reason);
}
uint256 gas1 = gasleft();
@@ -187,8 +230,8 @@ contract TestUniswapV3Sampler is Test, UniswapV3Common {
) private returns (uint256 uniQuoterGasUsage, uint256 multiQuoterGasUsage) {
uint256 gas0 = gasleft();
uint256[] memory multiQuoterAmountsIn;
try multiQuoter.quoteExactMultiOutput(factory, path, amountsOut) {} catch (bytes memory reason) {
(, multiQuoterAmountsIn, ) = catchMultiSwapResult(reason);
try multiQuoter.quoteExactMultiOutput(address(factory), path, amountsOut) {} catch (bytes memory reason) {
(, multiQuoterAmountsIn, ) = catchUniswapV3MultiSwapResult(reason);
}
uint256 gas1 = gasleft();
@@ -264,8 +307,8 @@ contract TestUniswapV3Sampler is Test, UniswapV3Common {
{
uint256[] memory mqGasEstimates;
try multiQuoter.quoteExactMultiInput(factory, path, amounts) {} catch (bytes memory reason) {
(, , mqGasEstimates) = catchMultiSwapResult(reason);
try multiQuoter.quoteExactMultiInput(address(factory), path, amounts) {} catch (bytes memory reason) {
(, , mqGasEstimates) = catchUniswapV3MultiSwapResult(reason);
}
for (uint256 i = 0; i < amounts.length; ++i) {
console.log("MQ Gas Estimates: i=%d, MQ: %d", i, mqGasEstimates[i]);
@@ -276,8 +319,8 @@ contract TestUniswapV3Sampler is Test, UniswapV3Common {
{
uint256[] memory mqGasEstimates;
try multiQuoter.quoteExactMultiInput(factory, path, amounts) {} catch (bytes memory reason) {
(, , mqGasEstimates) = catchMultiSwapResult(reason);
try multiQuoter.quoteExactMultiInput(address(factory), path, amounts) {} catch (bytes memory reason) {
(, , mqGasEstimates) = catchUniswapV3MultiSwapResult(reason);
}
for (uint256 i = 0; i < amounts.length; ++i) {
console.log("MQ Gas Estimates: i=%d, MQ: %d", i, mqGasEstimates[i]);