Skip to content

Commit

Permalink
quote exact input/output single
Browse files Browse the repository at this point in the history
  • Loading branch information
0xcivita committed Sep 29, 2024
1 parent c90e064 commit 8c9dbf2
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 2 deletions.
43 changes: 43 additions & 0 deletions contracts/interfaces/IV1LBQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity >=0.7.5;

import {IMarginalV1LBSupplier} from "./IMarginalV1LBSupplier.sol";
import {IV1LBRouter} from "./IV1LBRouter.sol";

/// @title The interface of the quoter for Marginal v1 liquidity bootstrapping pools
/// @notice Quotes the result of supplying and swaps on Marginal v1 liquidity bootstrapping pools
Expand Down Expand Up @@ -56,4 +57,46 @@ interface IV1LBQuoter {
uint160 sqrtPriceLowerX96,
uint160 sqrtPriceUpperX96
);

/// @notice Quotes the amountOut result of V1LBRouter::exactInputSingle
/// @param params Param inputs to V1LBRouter::exactInputSingle
/// @dev Reverts if exactInputSingle would revert
/// @return amountIn Amount of token sent to pool for swap
/// @return amountOut Amount of token received from pool after swap
/// @return liquidityAfter Pool liquidity after swap
/// @return sqrtPriceX96After Pool sqrt price after swap
/// @return finalizedAfter Whether the pool is finalized after swap
function quoteExactInputSingle(
IV1LBRouter.ExactInputSingleParams memory params
)
external
view
returns (
uint256 amountIn,
uint256 amountOut,
uint128 liquidityAfter,
uint160 sqrtPriceX96After,
bool finalizedAfter
);

/// @notice Quotes the amountIn result of V1LBRouter::exactOutputSingle
/// @param params Param inputs to V1LBRouter::exactOutputSingle
/// @dev Reverts if exactOutputSingle would revert
/// @return amountIn Amount of token sent to pool for swap
/// @return amountOut Amount of token received from pool after swap
/// @return liquidityAfter Pool liquidity after swap
/// @return sqrtPriceX96After Pool sqrt price after swap
/// @return finalizedAfter Whether the pool is finalized after swap
function quoteExactOutputSingle(
IV1LBRouter.ExactOutputSingleParams calldata params
)
external
view
returns (
uint256 amountIn,
uint256 amountOut,
uint128 liquidityAfter,
uint160 sqrtPriceX96After,
bool finalizedAfter
);
}
234 changes: 232 additions & 2 deletions contracts/lens/V1LBQuoter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,28 @@ pragma solidity =0.8.15;

import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
import {LiquidityAmounts} from "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol";
import {PeripheryValidation} from "@uniswap/v3-periphery/contracts/base/PeripheryValidation.sol";

import {SqrtPriceMath} from "@marginal/v1-core/contracts/libraries/SqrtPriceMath.sol";
import {SwapMath} from "@marginal/v1-core/contracts/libraries/SwapMath.sol";

import {PeripheryImmutableState} from "../base/PeripheryImmutableState.sol";

import {RangeMath} from "../libraries/RangeMath.sol";
import {PoolAddress} from "../libraries/PoolAddress.sol";
import {PoolConstants} from "../libraries/PoolConstants.sol";

import {IMarginalV1LBSupplier} from "../interfaces/IMarginalV1LBSupplier.sol";
import {IMarginalV1LBPool} from "../interfaces/IMarginalV1LBPool.sol";
import {IV1LBReceiverQuoter} from "../interfaces/receiver/IV1LBReceiverQuoter.sol";
import {IV1LBRouter} from "../interfaces/IV1LBRouter.sol";
import {IV1LBQuoter} from "../interfaces/IV1LBQuoter.sol";

contract V1LBQuoter is IV1LBQuoter {
contract V1LBQuoter is
IV1LBQuoter,
PeripheryImmutableState,
PeripheryValidation
{
/// @inheritdoc IV1LBQuoter
address public owner;

Expand All @@ -31,10 +44,21 @@ contract V1LBQuoter is IV1LBQuoter {

error Unauthorized();

constructor() {
constructor(
address _factory,
address _marginalV1Factory,
address _WETH9
) PeripheryImmutableState(_factory, _marginalV1Factory, _WETH9) {
owner = msg.sender;
}

/// @dev Returns the pool for the given token pair, and fee. The pool contract may or may not exist.
function getPool(
PoolAddress.PoolKey memory poolKey
) private view returns (IMarginalV1LBPool) {
return IMarginalV1LBPool(PoolAddress.getAddress(factory, poolKey));
}

/// @inheritdoc IV1LBQuoter
function setOwner(address _owner) external onlyOwner {
emit OwnerChanged(owner, _owner);
Expand Down Expand Up @@ -130,4 +154,210 @@ contract V1LBQuoter is IV1LBQuoter {
if (amount0 < params.amount0Min) revert("Amount0 less than min");
if (amount1 < params.amount1Min) revert("Amount1 less than min");
}

/// @inheritdoc IV1LBQuoter
function quoteExactInputSingle(
IV1LBRouter.ExactInputSingleParams memory params
)
external
view
checkDeadline(params.deadline)
returns (
uint256 amountIn,
uint256 amountOut,
uint128 liquidityAfter,
uint160 sqrtPriceX96After,
bool finalizedAfter
)
{
bool zeroForOne = params.tokenIn < params.tokenOut;
IMarginalV1LBPool pool = getPool(
PoolAddress.PoolKey({
token0: zeroForOne ? params.tokenIn : params.tokenOut,
token1: zeroForOne ? params.tokenOut : params.tokenIn,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
supplier: params.supplier,
blockTimestampInitialize: params.blockTimestampInitialize
})
);

(
uint160 sqrtPriceX96,
,
uint128 liquidity,
,
,
,
,
bool finalized
) = pool.state();
if (finalized) revert("Finalized");

uint160 sqrtPriceLimitX96 = params.sqrtPriceLimitX96 == 0
? (
zeroForOne
? TickMath.MIN_SQRT_RATIO + 1
: TickMath.MAX_SQRT_RATIO - 1
)
: params.sqrtPriceLimitX96;

if (
params.amountIn == 0 ||
params.amountIn >= uint256(type(uint256).max)
) revert("Invalid amountIn");
int256 amountSpecified = int256(params.amountIn);

if (
zeroForOne
? !(sqrtPriceLimitX96 < sqrtPriceX96 &&
sqrtPriceLimitX96 > SqrtPriceMath.MIN_SQRT_RATIO)
: !(sqrtPriceLimitX96 > sqrtPriceX96 &&
sqrtPriceLimitX96 < SqrtPriceMath.MAX_SQRT_RATIO)
) revert("Invalid sqrtPriceLimitX96");

uint160 sqrtPriceX96Next = SqrtPriceMath.sqrtPriceX96NextSwap(
liquidity,
sqrtPriceX96,
zeroForOne,
amountSpecified
);
if (
zeroForOne
? sqrtPriceX96Next < sqrtPriceLimitX96
: sqrtPriceX96Next > sqrtPriceLimitX96
) revert("sqrtPriceX96Next exceeds limit");

// clamp if exceeds lower or upper range limits
(uint160 sqrtPriceLowerX96, uint160 sqrtPriceUpperX96) = (
pool.sqrtPriceLowerX96(),
pool.sqrtPriceUpperX96()
);
bool clamped;
if (sqrtPriceX96Next < sqrtPriceLowerX96) {
sqrtPriceX96Next = sqrtPriceLowerX96;
clamped = true;
} else if (sqrtPriceX96Next > sqrtPriceUpperX96) {
sqrtPriceX96Next = sqrtPriceUpperX96;
clamped = true;
}

// amounts without fees
(int256 amount0, int256 amount1) = SwapMath.swapAmounts(
liquidity,
sqrtPriceX96,
sqrtPriceX96Next
);
amountOut = uint256(-(zeroForOne ? amount1 : amount0));
if (amountOut < params.amountOutMinimum) revert("Too little received");

// account for clamping
amountIn = !clamped
? params.amountIn
: uint256(zeroForOne ? amount0 : amount1);

// calculate liquidity, sqrtP, finalized after
liquidityAfter = liquidity;
sqrtPriceX96After = sqrtPriceX96Next;
finalizedAfter = (sqrtPriceX96Next == pool.sqrtPriceFinalizeX96());
}

/// @inheritdoc IV1LBQuoter
function quoteExactOutputSingle(
IV1LBRouter.ExactOutputSingleParams memory params
)
external
view
checkDeadline(params.deadline)
returns (
uint256 amountIn,
uint256 amountOut,
uint128 liquidityAfter,
uint160 sqrtPriceX96After,
bool finalizedAfter
)
{
bool zeroForOne = params.tokenIn < params.tokenOut;
IMarginalV1LBPool pool = getPool(
PoolAddress.PoolKey({
token0: zeroForOne ? params.tokenIn : params.tokenOut,
token1: zeroForOne ? params.tokenOut : params.tokenIn,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
supplier: params.supplier,
blockTimestampInitialize: params.blockTimestampInitialize
})
);

(
uint160 sqrtPriceX96,
,
uint128 liquidity,
,
,
,
,
bool finalized
) = pool.state();
if (finalized) revert("Finalized");

uint160 sqrtPriceLimitX96 = params.sqrtPriceLimitX96 == 0
? (
zeroForOne
? TickMath.MIN_SQRT_RATIO + 1
: TickMath.MAX_SQRT_RATIO - 1
)
: params.sqrtPriceLimitX96;

if (
params.amountOut == 0 ||
params.amountOut >= uint256(type(uint256).max)
) revert("Invalid amountOut");
int256 amountSpecified = -int256(params.amountOut);

if (
zeroForOne
? !(sqrtPriceLimitX96 < sqrtPriceX96 &&
sqrtPriceLimitX96 > SqrtPriceMath.MIN_SQRT_RATIO)
: !(sqrtPriceLimitX96 > sqrtPriceX96 &&
sqrtPriceLimitX96 < SqrtPriceMath.MAX_SQRT_RATIO)
) revert("Invalid sqrtPriceLimitX96");

uint160 sqrtPriceX96Next = SqrtPriceMath.sqrtPriceX96NextSwap(
liquidity,
sqrtPriceX96,
zeroForOne,
amountSpecified
);
if (
zeroForOne
? sqrtPriceX96Next < sqrtPriceLimitX96
: sqrtPriceX96Next > sqrtPriceLimitX96
) revert("sqrtPriceX96Next exceeds limit");

// error if exceeds lower or upper range limits
(uint160 sqrtPriceLowerX96, uint160 sqrtPriceUpperX96) = (
pool.sqrtPriceLowerX96(),
pool.sqrtPriceUpperX96()
);
if (
sqrtPriceX96Next < sqrtPriceLowerX96 ||
sqrtPriceX96Next > sqrtPriceUpperX96
) revert("Invalid sqrtPriceX96Next");

// amounts without fees
(int256 amount0, int256 amount1) = SwapMath.swapAmounts(
liquidity,
sqrtPriceX96,
sqrtPriceX96Next
);
amountOut = uint256(-amountSpecified);
amountIn = uint256(zeroForOne ? amount0 : amount1);
if (amountIn > params.amountInMaximum) revert("Too much requested");

// calculate liquidity, sqrtP, finalized after
liquidityAfter = liquidity;
sqrtPriceX96After = sqrtPriceX96Next;
finalizedAfter = (sqrtPriceX96Next == pool.sqrtPriceFinalizeX96());
}
}
12 changes: 12 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ def margv1_supplier(
)


@pytest.fixture(scope="module")
def margv1_quoter(
assert_mainnet_fork, project, accounts, factory, margv1_factory, WETH9
):
return project.V1LBQuoter.deploy(
factory.address,
margv1_factory.address,
WETH9.address,
sender=accounts[0],
)


@pytest.fixture(scope="module")
def margv1_ticks(
assert_mainnet_fork,
Expand Down
53 changes: 53 additions & 0 deletions tests/integration/quoter/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pytest


@pytest.fixture(scope="module")
def finalizer(accounts):
yield accounts[4]


@pytest.fixture(scope="module")
def treasury(accounts):
yield accounts[5]


@pytest.fixture(scope="module")
def margv1_liquidity_receiver_deployer(
assert_mainnet_fork,
project,
accounts,
univ3_manager,
margv1_factory,
margv1_initializer,
margv1_router,
margv1_supplier,
WETH9,
):
return project.MarginalV1LBLiquidityReceiverDeployer.deploy(
margv1_supplier.address,
univ3_manager.address,
margv1_factory.address,
margv1_initializer.address,
margv1_router.address,
WETH9.address,
sender=accounts[0],
)


@pytest.fixture(scope="module")
def margv1_receiver_params(finalizer, treasury, sender):
return (
treasury.address, # treasuryAddress
int(0.1e6), # treasuryRatio: 10% to treasury
int(
0.5e6
), # uniswapV3Ratio: 50% to univ3 pool and 50% to margv1 pool less treasury
3000, # uniswapV3Fee
250000, # marginalV1Maintenance
finalizer.address, # lockOwner
int(86400 * 30), # lockDuration: 30 days
sender.address, # refundAddress
)


# TODO: receiver quoter
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

0 comments on commit 8c9dbf2

Please sign in to comment.