Price-based slippage calculation for increase liquidity (#135)

- Deprecated the old increaseLiquidityQuoteByInputToken quote function & added a new increaseLiquidityQuoteByInputTokenUsingPriceSlippage to calculate tokenMax based on price movement
- Adding increaseLiquidityQuoteByLiquidity quote function to generate a quote by a set liquidity value
This commit is contained in:
meep 2024-03-08 03:21:55 +09:00 committed by GitHub
parent 6f7277c2f7
commit d327356343
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1203 additions and 163 deletions

View File

@ -17,6 +17,9 @@ import {
import { PriceMath, TickUtil } from "../../utils/public";
import { Whirlpool } from "../../whirlpool-client";
/*** --------- Quote by Input Token --------- ***/
/**
* @category Quotes
* @param inputTokenAmount - The amount of input tokens to deposit.
@ -45,7 +48,208 @@ export type IncreaseLiquidityQuoteParam = {
* Return object from increase liquidity quote functions.
* @category Quotes
*/
export type IncreaseLiquidityQuote = IncreaseLiquidityInput & { tokenEstA: BN; tokenEstB: BN };
export type IncreaseLiquidityQuote = IncreaseLiquidityInput & IncreaseLiquidityEstimate;
type IncreaseLiquidityEstimate = { liquidityAmount: BN; tokenEstA: BN; tokenEstB: BN };
/**
* Get an estimated quote on the maximum tokens required to deposit based on a specified input token amount.
* This new version calculates slippage based on price percentage movement, rather than setting the percentage threshold based on token estimates.
*
* @category Quotes
* @param inputTokenAmount - The amount of input tokens to deposit.
* @param inputTokenMint - The mint of the input token the user would like to deposit.
* @param tickLower - The lower index of the position that we are depositing into.
* @param tickUpper - The upper index of the position that we are depositing into.
* @param slippageTolerance - The maximum slippage allowed when calculating the minimum tokens received.
* @param whirlpool - A Whirlpool helper class to help interact with the Whirlpool account.
* @returns An IncreaseLiquidityInput object detailing the required token amounts & liquidity values to use when calling increase-liquidity-ix.
*/
export function increaseLiquidityQuoteByInputTokenUsingPriceSlippage(
inputTokenMint: Address,
inputTokenAmount: Decimal,
tickLower: number,
tickUpper: number,
slippageTolerance: Percentage,
whirlpool: Whirlpool
) {
const data = whirlpool.getData();
const tokenAInfo = whirlpool.getTokenAInfo();
const tokenBInfo = whirlpool.getTokenBInfo();
const inputMint = AddressUtil.toPubKey(inputTokenMint);
const inputTokenInfo = inputMint.equals(tokenAInfo.mint) ? tokenAInfo : tokenBInfo;
return increaseLiquidityQuoteByInputTokenWithParamsUsingPriceSlippage({
inputTokenMint: inputMint,
inputTokenAmount: DecimalUtil.toBN(inputTokenAmount, inputTokenInfo.decimals),
tickLowerIndex: TickUtil.getInitializableTickIndex(tickLower, data.tickSpacing),
tickUpperIndex: TickUtil.getInitializableTickIndex(tickUpper, data.tickSpacing),
slippageTolerance,
...data,
});
}
/**
* Get an estimated quote on the maximum tokens required to deposit based on a specified input token amount.
* This new version calculates slippage based on price percentage movement, rather than setting the percentage threshold based on token estimates.
*
* @category Quotes
* @param param IncreaseLiquidityQuoteParam
* @returns An IncreaseLiquidityInput object detailing the required token amounts & liquidity values to use when calling increase-liquidity-ix.
*/
export function increaseLiquidityQuoteByInputTokenWithParamsUsingPriceSlippage(
param: IncreaseLiquidityQuoteParam
): IncreaseLiquidityQuote {
invariant(TickUtil.checkTickInBounds(param.tickLowerIndex), "tickLowerIndex is out of bounds.");
invariant(TickUtil.checkTickInBounds(param.tickUpperIndex), "tickUpperIndex is out of bounds.");
invariant(
param.inputTokenMint.equals(param.tokenMintA) || param.inputTokenMint.equals(param.tokenMintB),
`input token mint ${param.inputTokenMint.toBase58()} does not match any tokens in the provided pool.`
);
const liquidity = getLiquidityFromInputToken(param);
if (liquidity.eq(ZERO)) {
return {
tokenMaxA: ZERO,
tokenMaxB: ZERO,
liquidityAmount: ZERO,
tokenEstA: ZERO,
tokenEstB: ZERO,
};
}
return increaseLiquidityQuoteByLiquidityWithParams({
liquidity,
tickCurrentIndex: param.tickCurrentIndex,
sqrtPrice: param.sqrtPrice,
tickLowerIndex: param.tickLowerIndex,
tickUpperIndex: param.tickUpperIndex,
slippageTolerance: param.slippageTolerance,
});
}
function getLiquidityFromInputToken(params: IncreaseLiquidityQuoteParam) {
const { inputTokenMint, inputTokenAmount, tickLowerIndex, tickUpperIndex, tickCurrentIndex, sqrtPrice } = params;
invariant(tickLowerIndex < tickUpperIndex, `tickLowerIndex(${tickLowerIndex}) must be less than tickUpperIndex(${tickUpperIndex})`);
if (inputTokenAmount.eq(ZERO)) {
return ZERO;
}
const isTokenA = params.tokenMintA.equals(inputTokenMint);
const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
const positionStatus = PositionUtil.getStrictPositionStatus(sqrtPrice, tickLowerIndex, tickUpperIndex);
if (positionStatus === PositionStatus.BelowRange) {
return isTokenA ? getLiquidityFromTokenA(inputTokenAmount, sqrtPriceLowerX64, sqrtPriceUpperX64, false) : ZERO;
}
if (positionStatus === PositionStatus.AboveRange) {
return isTokenA ? ZERO : getLiquidityFromTokenB(inputTokenAmount, sqrtPriceLowerX64, sqrtPriceUpperX64, false);
}
return isTokenA
? getLiquidityFromTokenA(inputTokenAmount, sqrtPrice, sqrtPriceUpperX64, false)
: getLiquidityFromTokenB(inputTokenAmount, sqrtPriceLowerX64, sqrtPrice, false);
}
/*** --------- Quote by Liquidity --------- ***/
/**
* @category Quotes
* @param liquidity - The amount of liquidity value to deposit into the Whirlpool.
* @param tokenMintA - The mint of tokenA in the Whirlpool the user is depositing into.
* @param tokenMintB -The mint of tokenB in the Whirlpool the user is depositing into.
* @param tickCurrentIndex - The Whirlpool's current tickIndex
* @param sqrtPrice - The Whirlpool's current sqrtPrice
* @param tickLowerIndex - The lower index of the position that we are withdrawing from.
* @param tickUpperIndex - The upper index of the position that we are withdrawing from.
* @param slippageTolerance - The maximum slippage allowed when calculating the minimum tokens received.
*/
export type IncreaseLiquidityQuoteByLiquidityParam = {
liquidity: BN;
tickCurrentIndex: number;
sqrtPrice: BN;
tickLowerIndex: number;
tickUpperIndex: number;
slippageTolerance: Percentage;
};
export function increaseLiquidityQuoteByLiquidityWithParams(params: IncreaseLiquidityQuoteByLiquidityParam): IncreaseLiquidityQuote {
if (params.liquidity.eq(ZERO)) {
return {
tokenMaxA: ZERO,
tokenMaxB: ZERO,
liquidityAmount: ZERO,
tokenEstA: ZERO,
tokenEstB: ZERO,
};
}
const { tokenEstA, tokenEstB } = getTokenEstimatesFromLiquidity(params);
const {
lowerBound: [sLowerSqrtPrice, sLowerIndex],
upperBound: [sUpperSqrtPrice, sUpperIndex],
} = PriceMath.getSlippageBoundForSqrtPrice(params.sqrtPrice, params.slippageTolerance);
const { tokenEstA: tokenEstALower, tokenEstB: tokenEstBLower } = getTokenEstimatesFromLiquidity({
...params,
sqrtPrice: sLowerSqrtPrice,
tickCurrentIndex: sLowerIndex,
});
const { tokenEstA: tokenEstAUpper, tokenEstB: tokenEstBUpper } = getTokenEstimatesFromLiquidity({
...params,
sqrtPrice: sUpperSqrtPrice,
tickCurrentIndex: sUpperIndex,
});
const tokenMaxA = BN.max(BN.max(tokenEstA, tokenEstALower), tokenEstAUpper);
const tokenMaxB = BN.max(BN.max(tokenEstB, tokenEstBLower), tokenEstBUpper);
return {
tokenMaxA,
tokenMaxB,
tokenEstA,
tokenEstB,
liquidityAmount: params.liquidity,
};
}
function getTokenEstimatesFromLiquidity(params: IncreaseLiquidityQuoteByLiquidityParam) {
const {
liquidity,
sqrtPrice,
tickLowerIndex,
tickUpperIndex
} = params;
if (liquidity.eq(ZERO)) {
throw new Error("liquidity must be greater than 0");
}
let tokenEstA = ZERO;
let tokenEstB = ZERO;
const lowerSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const upperSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
const positionStatus = PositionUtil.getStrictPositionStatus(sqrtPrice, tickLowerIndex, tickUpperIndex);
if (positionStatus === PositionStatus.BelowRange) {
tokenEstA = getTokenAFromLiquidity(liquidity, lowerSqrtPrice, upperSqrtPrice, true);
} else if (positionStatus === PositionStatus.InRange) {
tokenEstA = getTokenAFromLiquidity(liquidity, sqrtPrice, upperSqrtPrice, true);
tokenEstB = getTokenBFromLiquidity(liquidity, lowerSqrtPrice, sqrtPrice, true);
} else {
tokenEstB = getTokenBFromLiquidity(liquidity, lowerSqrtPrice, upperSqrtPrice, true);
}
return { tokenEstA, tokenEstB };
}
/*** --------- Deprecated --------- ***/
/**
* Get an estimated quote on the maximum tokens required to deposit based on a specified input token amount.
@ -58,6 +262,7 @@ export type IncreaseLiquidityQuote = IncreaseLiquidityInput & { tokenEstA: BN; t
* @param slippageTolerance - The maximum slippage allowed when calculating the minimum tokens received.
* @param whirlpool - A Whirlpool helper class to help interact with the Whirlpool account.
* @returns An IncreaseLiquidityInput object detailing the required token amounts & liquidity values to use when calling increase-liquidity-ix.
* @deprecated Use increaseLiquidityQuoteByInputTokenUsingPriceSlippage instead.
*/
export function increaseLiquidityQuoteByInputToken(
inputTokenMint: Address,
@ -79,7 +284,7 @@ export function increaseLiquidityQuoteByInputToken(
inputTokenAmount: DecimalUtil.toBN(inputTokenAmount, inputTokenInfo.decimals),
tickLowerIndex: TickUtil.getInitializableTickIndex(tickLower, data.tickSpacing),
tickUpperIndex: TickUtil.getInitializableTickIndex(tickUpper, data.tickSpacing),
slippageTolerance,
slippageTolerance: slippageTolerance,
...data,
});
}
@ -90,6 +295,7 @@ export function increaseLiquidityQuoteByInputToken(
* @category Quotes
* @param param IncreaseLiquidityQuoteParam
* @returns An IncreaseLiquidityInput object detailing the required token amounts & liquidity values to use when calling increase-liquidity-ix.
* @deprecated Use increaseLiquidityQuoteByInputTokenWithParams_PriceSlippage instead.
*/
export function increaseLiquidityQuoteByInputTokenWithParams(
param: IncreaseLiquidityQuoteParam
@ -119,8 +325,9 @@ export function increaseLiquidityQuoteByInputTokenWithParams(
}
}
/*** Private ***/
/**
* @deprecated
*/
function quotePositionBelowRange(param: IncreaseLiquidityQuoteParam): IncreaseLiquidityQuote {
const {
tokenMintA,
@ -168,6 +375,9 @@ function quotePositionBelowRange(param: IncreaseLiquidityQuoteParam): IncreaseLi
};
}
/**
* @deprecated
*/
function quotePositionInRange(param: IncreaseLiquidityQuoteParam): IncreaseLiquidityQuote {
const {
tokenMintA,
@ -213,6 +423,9 @@ function quotePositionInRange(param: IncreaseLiquidityQuoteParam): IncreaseLiqui
};
}
/**
* @deprecated
*/
function quotePositionAboveRange(param: IncreaseLiquidityQuoteParam): IncreaseLiquidityQuote {
const {
tokenMintB,
@ -257,4 +470,4 @@ function quotePositionAboveRange(param: IncreaseLiquidityQuoteParam): IncreaseLi
tokenEstB,
liquidityAmount,
};
}
}

View File

@ -51,6 +51,18 @@ export const MAX_SQRT_PRICE = "79226673515401279992447579055";
*/
export const MIN_SQRT_PRICE = "4295048016";
/**
* The minimum sqrt-price supported by the Whirlpool program.
* @category Constants
*/
export const MIN_SQRT_PRICE_BN = new BN(MIN_SQRT_PRICE);
/**
* The maximum sqrt-price supported by the Whirlpool program.
* @category Constants
*/
export const MAX_SQRT_PRICE_BN = new BN(MAX_SQRT_PRICE);
/**
* The number of initialized ticks that a tick-array account can hold.
* @category Constants

View File

@ -1,12 +1,12 @@
import { BN } from "@coral-xyz/anchor";
import { MathUtil, Percentage } from "@orca-so/common-sdk";
import { PriceMath } from "./public";
import {
getLowerSqrtPriceFromTokenA,
getLowerSqrtPriceFromTokenB,
getUpperSqrtPriceFromTokenA,
getUpperSqrtPriceFromTokenB,
} from "./swap-utils";
import { PriceMath } from "./public";
export enum SwapDirection {
AtoB = "Swap A to B",
@ -25,7 +25,7 @@ export enum PositionStatus {
}
export class PositionUtil {
private constructor() {}
private constructor() { }
/**
* Returns the position status of a given tickCurrentIndex in relation to the tickLowerIndex and tickUpperIndex.
@ -72,7 +72,7 @@ export class PositionUtil {
): PositionStatus {
const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
if (sqrtPriceX64.lte(sqrtPriceLowerX64)) {
return PositionStatus.BelowRange;
} else if (sqrtPriceX64.gte(sqrtPriceUpperX64)) {

View File

@ -1,7 +1,12 @@
import { BN } from "@coral-xyz/anchor";
import { MathUtil } from "@orca-so/common-sdk";
import { DecimalUtil, MathUtil, Percentage } from "@orca-so/common-sdk";
import Decimal from "decimal.js";
import { MAX_SQRT_PRICE, MIN_SQRT_PRICE } from "../../types/public";
import {
MAX_SQRT_PRICE,
MAX_SQRT_PRICE_BN,
MIN_SQRT_PRICE,
MIN_SQRT_PRICE_BN,
} from "../../types/public";
import { TickUtil } from "./tick-utils";
const BIT_PRECISION = 14;
@ -22,7 +27,7 @@ export class PriceMath {
public static sqrtPriceX64ToPrice(
sqrtPriceX64: BN,
decimalsA: number,
decimalsB: number
decimalsB: number,
): Decimal {
return MathUtil.fromX64(sqrtPriceX64)
.pow(2)
@ -78,12 +83,12 @@ export class PriceMath {
const tickLow = signedShiftRight(
logbpX64.sub(new BN(LOG_B_P_ERR_MARGIN_LOWER_X64)),
64,
128
128,
).toNumber();
const tickHigh = signedShiftRight(
logbpX64.add(new BN(LOG_B_P_ERR_MARGIN_UPPER_X64)),
64,
128
128,
).toNumber();
if (tickLow == tickHigh) {
@ -102,13 +107,13 @@ export class PriceMath {
return PriceMath.sqrtPriceX64ToPrice(
PriceMath.tickIndexToSqrtPriceX64(tickIndex),
decimalsA,
decimalsB
decimalsB,
);
}
public static priceToTickIndex(price: Decimal, decimalsA: number, decimalsB: number): number {
return PriceMath.sqrtPriceX64ToTickIndex(
PriceMath.priceToSqrtPriceX64(price, decimalsA, decimalsB)
PriceMath.priceToSqrtPriceX64(price, decimalsA, decimalsB),
);
}
@ -116,11 +121,11 @@ export class PriceMath {
price: Decimal,
decimalsA: number,
decimalsB: number,
tickSpacing: number
tickSpacing: number,
): number {
return TickUtil.getInitializableTickIndex(
PriceMath.priceToTickIndex(price, decimalsA, decimalsB),
tickSpacing
tickSpacing,
);
}
@ -151,6 +156,48 @@ export class PriceMath {
const invTick = TickUtil.invertTick(tick);
return PriceMath.tickIndexToSqrtPriceX64(invTick);
}
/**
* Calculate the sqrtPriceX64 & tick index slippage price boundary for a given price and slippage.
* Note: This function loses precision
*
* @param sqrtPriceX64 the sqrtPriceX64 to apply the slippage on
* @param slippage the slippage to apply onto the sqrtPriceX64
* @returns the sqrtPriceX64 & tick index slippage price boundary
*/
public static getSlippageBoundForSqrtPrice(
sqrtPriceX64: BN,
slippage: Percentage,
): { lowerBound: [BN, number]; upperBound: [BN, number] } {
const sqrtPriceX64Decimal = DecimalUtil.fromBN(sqrtPriceX64);
const slippageNumerator = new Decimal(slippage.numerator.toString());
const slippageDenominator = new Decimal(slippage.denominator.toString());
const lowerBoundSqrtPriceDecimal = sqrtPriceX64Decimal
.mul(slippageDenominator.sub(slippageNumerator).sqrt())
.div(slippageDenominator.sqrt())
.toDecimalPlaces(0);
const upperBoundSqrtPriceDecimal = sqrtPriceX64Decimal
.mul(slippageDenominator.add(slippageNumerator).sqrt())
.div(slippageDenominator.sqrt())
.toDecimalPlaces(0);
const lowerBoundSqrtPrice = BN.min(
BN.max(new BN(lowerBoundSqrtPriceDecimal.toString()), MIN_SQRT_PRICE_BN),
MAX_SQRT_PRICE_BN,
);
const upperBoundSqrtPrice = BN.min(
BN.max(new BN(upperBoundSqrtPriceDecimal.toString()), MIN_SQRT_PRICE_BN),
MAX_SQRT_PRICE_BN,
);
const lowerTickCurrentIndex = PriceMath.sqrtPriceX64ToTickIndex(lowerBoundSqrtPrice);
const upperTickCurrentIndex = PriceMath.sqrtPriceX64ToTickIndex(upperBoundSqrtPrice);
return {
lowerBound: [lowerBoundSqrtPrice, lowerTickCurrentIndex],
upperBound: [upperBoundSqrtPrice, upperTickCurrentIndex],
};
}
}
// Private Functions

View File

@ -9,7 +9,7 @@ import {
WhirlpoolContext,
WhirlpoolIx,
buildWhirlpoolClient,
increaseLiquidityQuoteByInputTokenWithParams,
increaseLiquidityQuoteByInputTokenWithParamsUsingPriceSlippage,
toTx
} from "../../src";
import { IGNORE_CACHE } from "../../src/network/public/fetcher";
@ -196,7 +196,7 @@ describe("close_bundled_position", () => {
// deposit
const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey, IGNORE_CACHE);
const quote = increaseLiquidityQuoteByInputTokenWithParams({
const quote = increaseLiquidityQuoteByInputTokenWithParamsUsingPriceSlippage({
tokenMintA: poolInitInfo.tokenMintA,
tokenMintB: poolInitInfo.tokenMintB,
sqrtPrice: pool.getData().sqrtPrice,

View File

@ -6,7 +6,7 @@ import Decimal from "decimal.js";
import {
buildWhirlpoolClient,
decreaseLiquidityQuoteByLiquidity,
increaseLiquidityQuoteByInputToken,
increaseLiquidityQuoteByInputTokenUsingPriceSlippage,
PriceMath
} from "../../../src";
import { WhirlpoolContext } from "../../../src/context";
@ -71,7 +71,7 @@ describe("position-impl", () => {
// [Action] Increase liquidity by 70 tokens of tokenB
const position = await client.getPosition(positionAddress.publicKey, IGNORE_CACHE);
const preIncreaseData = position.getData();
const increase_quote = increaseLiquidityQuoteByInputToken(
const increase_quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage(
poolInitInfo.tokenMintB,
new Decimal(70),
lowerTick,
@ -155,7 +155,7 @@ describe("position-impl", () => {
// [Action] Increase liquidity by 70 tokens of tokenB & create the ATA in the new source Wallet
const position = await client.getPosition(positionAddress.publicKey, IGNORE_CACHE);
const preIncreaseData = position.getData();
const increase_quote = increaseLiquidityQuoteByInputToken(
const increase_quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage(
poolInitInfo.tokenMintB,
new Decimal(70),
lowerTick,
@ -201,7 +201,7 @@ describe("position-impl", () => {
10_500_000_000,
otherWallet.publicKey
);
const increaseQuoteFromOtherWallet = increaseLiquidityQuoteByInputToken(
const increaseQuoteFromOtherWallet = increaseLiquidityQuoteByInputTokenUsingPriceSlippage(
poolInitInfo.tokenMintB,
new Decimal(80),
lowerTick,

View File

@ -0,0 +1,450 @@
import { Percentage, ZERO } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
import * as assert from "assert";
import BN from "bn.js";
import {
PriceMath,
increaseLiquidityQuoteByInputTokenWithParams,
increaseLiquidityQuoteByInputTokenWithParamsUsingPriceSlippage,
increaseLiquidityQuoteByLiquidityWithParams,
} from "../../../../src";
import {
getLiquidityFromTokenA,
getLiquidityFromTokenB,
} from "../../../../src/utils/position-util";
function getTestSlippageRange(currIndex: number, slippage: Percentage) {
const sqrtPrice = PriceMath.tickIndexToSqrtPriceX64(currIndex);
const {
lowerBound: [_sLowerSqrtPrice, sLowerIndex],
upperBound: [_sUpperSqrtPrice, sUpperIndex],
} = PriceMath.getSlippageBoundForSqrtPrice(sqrtPrice, slippage);
return {
tickLowerIndex: sLowerIndex === sUpperIndex ? sLowerIndex - 1 : sLowerIndex,
tickUpperIndex: sUpperIndex,
tickCurrentIndex: currIndex,
};
}
const variations = [
[0, true, Percentage.fromFraction(1, 1000)] as const,
[0, false, Percentage.fromFraction(1, 1000)] as const,
[0, true, Percentage.fromFraction(1, 100)] as const,
[0, false, Percentage.fromFraction(1, 100)] as const,
[234653, true, Percentage.fromFraction(1, 1000)] as const,
[234653, false, Percentage.fromFraction(1, 1000)] as const,
[234653, true, Percentage.fromFraction(1, 100)] as const,
[234653, false, Percentage.fromFraction(1, 100)] as const,
[-234653, true, Percentage.fromFraction(1, 1000)] as const,
[-234653, false, Percentage.fromFraction(1, 1000)] as const,
[-234653, true, Percentage.fromFraction(1, 100)] as const,
[-234653, false, Percentage.fromFraction(1, 100)] as const,
];
// NOTE: Slippage range for current price (tick = 0) is [-101, 99]
// [---P---] = P is the current price & [] is the slippage boundary
// |-------| = Position Boundary
variations.forEach(([currentTickIndex, isTokenA, slippage]) => {
describe("increaseLiquidityQuoteByInputTokenUsingPriceSlippage", () => {
const tokenMintA = new PublicKey("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE");
const tokenMintB = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
it(`|[--------P--------]| @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex,
pTickUpperIndex: slippageRange.tickCurrentIndex,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`|----------------| [---P---] @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex - 200,
pTickUpperIndex: slippageRange.tickLowerIndex - 100,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`|--------------[--|--P----] @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex - 200,
pTickUpperIndex: slippageRange.tickLowerIndex + 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`[|---|---P------] @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex,
pTickUpperIndex: slippageRange.tickCurrentIndex - 1,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`[--|---|--P-------] @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex + 5,
pTickUpperIndex: slippageRange.tickCurrentIndex - 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`|-----[---P---]-----| @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex - 200,
pTickUpperIndex: slippageRange.tickUpperIndex + 200,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`[--|----P----]-----| @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex + 5,
pTickUpperIndex: slippageRange.tickUpperIndex + 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`|--[---P---|-----] @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex - 125,
pTickUpperIndex: slippageRange.tickCurrentIndex + 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`[---|---P---|----] @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex + 5,
pTickUpperIndex: slippageRange.tickCurrentIndex + 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`[---P---] |---------| @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickUpperIndex + 100,
pTickUpperIndex: slippageRange.tickUpperIndex + 200,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`[---P--|---]------| @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickCurrentIndex + 5,
pTickUpperIndex: slippageRange.tickUpperIndex + 100,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`[-----P--|---|] @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickCurrentIndex + 5,
pTickUpperIndex: slippageRange.tickUpperIndex,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
it(`[-------P--|---|--] @ isTokenA - ${isTokenA} tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlippageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickCurrentIndex + 2,
pTickUpperIndex: slippageRange.tickUpperIndex - 2,
tickCurrentIndex: slippageRange.tickCurrentIndex,
inputTokenAmount: new BN(100000),
isTokenA,
slippageTolerance: slippage,
});
});
function testVariation(params: {
pTickLowerIndex: number;
pTickUpperIndex: number;
tickCurrentIndex: number;
inputTokenAmount: BN;
isTokenA: boolean;
slippageTolerance: Percentage;
}) {
const { pTickLowerIndex, pTickUpperIndex, tickCurrentIndex, inputTokenAmount, isTokenA } =
params;
const sqrtPrice = PriceMath.tickIndexToSqrtPriceX64(tickCurrentIndex);
const inputTokenMint = isTokenA ? tokenMintA : tokenMintB;
const quote = increaseLiquidityQuoteByInputTokenWithParamsUsingPriceSlippage({
inputTokenAmount,
inputTokenMint,
sqrtPrice,
tokenMintA,
tokenMintB,
tickLowerIndex: pTickLowerIndex,
tickUpperIndex: pTickUpperIndex,
tickCurrentIndex,
slippageTolerance: slippage,
});
// Expectations
const liquidity = getLiquidityFromInputToken({
inputTokenAmount,
isInputTokenA: isTokenA,
sqrtPrice,
currentTickIndex: tickCurrentIndex,
lowerTickIndex: pTickLowerIndex,
upperTickIndex: pTickUpperIndex,
});
const expectedQuote = increaseLiquidityQuoteByLiquidityWithParams({
tickLowerIndex: pTickLowerIndex,
tickUpperIndex: pTickUpperIndex,
tickCurrentIndex,
liquidity,
sqrtPrice: sqrtPrice,
slippageTolerance: slippage,
});
const {
tokenEstA: expectedTokenEstA,
tokenEstB: expectedTokenEstB,
tokenMaxA: expectedTokenMaxA,
tokenMaxB: expectedTokenMaxB,
} = expectedQuote;
assert.ok(
quote.tokenEstA.eq(expectedTokenEstA),
`tokenEstA: ${quote.tokenEstA.toString()} !== ${expectedTokenEstA.toString()}`,
);
assert.ok(
quote.tokenEstB.eq(expectedTokenEstB),
`tokenEstB: ${quote.tokenEstB.toString()} !== ${expectedTokenEstB.toString()}`,
);
assert.ok(
quote.tokenMaxA.eq(expectedTokenMaxA),
`tokenMaxA: ${quote.tokenMaxA.toString()} !== ${expectedTokenMaxA.toString()}`,
);
assert.ok(
quote.tokenMaxB.eq(expectedTokenMaxB),
`tokenMaxB: ${quote.tokenMaxB.toString()} !== ${expectedTokenMaxB.toString()}`,
);
}
});
});
describe("edge cases for old slippage", () => {
const tokenMintA = new PublicKey("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE");
const tokenMintB = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
it("sqrtPrice on lower bound, tokenB input", async () => {
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintB,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(0),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 0,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.isZero());
assert.ok(quote.tokenEstA.isZero());
assert.ok(quote.tokenEstB.isZero());
assert.ok(quote.tokenMaxA.isZero());
assert.ok(quote.tokenMaxB.isZero());
});
it("sqrtPrice on lower bound, tokenA input", async () => {
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintA,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(0),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 0,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.gtn(0));
assert.ok(quote.tokenEstA.gtn(0));
assert.ok(quote.tokenEstB.isZero());
assert.ok(quote.tokenMaxA.gtn(0));
assert.ok(quote.tokenMaxB.isZero());
});
it("tickCurrentIndex on lower bound but sqrtPrice not on lower bound, tokenA input", async () => {
assert.ok(
PriceMath.tickIndexToSqrtPriceX64(1).subn(1).gt(PriceMath.tickIndexToSqrtPriceX64(0)),
);
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintA,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(1).subn(1),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 0,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.gtn(0));
assert.ok(quote.tokenEstA.gtn(0));
assert.ok(quote.tokenEstB.gtn(0));
assert.ok(quote.tokenMaxA.gtn(0));
assert.ok(quote.tokenMaxB.gtn(0));
});
it("tickCurrentIndex on lower bound but sqrtPrice not on lower bound, tokenB input", async () => {
assert.ok(
PriceMath.tickIndexToSqrtPriceX64(1).subn(1).gt(PriceMath.tickIndexToSqrtPriceX64(0)),
);
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintB,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(1).subn(1),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 0,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.gtn(0));
assert.ok(quote.tokenEstA.gtn(0));
assert.ok(quote.tokenEstB.gtn(0));
assert.ok(quote.tokenMaxA.gtn(0));
assert.ok(quote.tokenMaxB.gtn(0));
});
it("sqrtPrice on upper bound, tokenA input", async () => {
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintA,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(64),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 64,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.isZero());
assert.ok(quote.tokenEstA.isZero());
assert.ok(quote.tokenEstB.isZero());
assert.ok(quote.tokenMaxA.isZero());
assert.ok(quote.tokenMaxB.isZero());
});
it("sqrtPrice on upper bound, tokenB input", async () => {
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintB,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(64),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 64,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.gtn(0));
assert.ok(quote.tokenEstA.isZero());
assert.ok(quote.tokenEstB.gtn(0));
assert.ok(quote.tokenMaxA.isZero());
assert.ok(quote.tokenMaxB.gtn(0));
});
});
function getLiquidityFromInputToken(params: {
inputTokenAmount: BN;
isInputTokenA: boolean;
currentTickIndex: number;
sqrtPrice: BN;
lowerTickIndex: number;
upperTickIndex: number;
}) {
const {
inputTokenAmount,
isInputTokenA,
sqrtPrice,
currentTickIndex,
lowerTickIndex,
upperTickIndex,
} = params;
const lowerSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(lowerTickIndex);
const upperSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(upperTickIndex);
if (currentTickIndex >= upperTickIndex) {
return isInputTokenA
? ZERO
: getLiquidityFromTokenB(inputTokenAmount, lowerSqrtPrice, upperSqrtPrice, false);
}
if (currentTickIndex < lowerTickIndex) {
return isInputTokenA
? getLiquidityFromTokenA(inputTokenAmount, lowerSqrtPrice, upperSqrtPrice, false)
: ZERO;
}
return isInputTokenA
? getLiquidityFromTokenA(inputTokenAmount, sqrtPrice, upperSqrtPrice, false)
: getLiquidityFromTokenB(inputTokenAmount, lowerSqrtPrice, sqrtPrice, false);
}

View File

@ -0,0 +1,327 @@
import { Percentage, ZERO } from "@orca-so/common-sdk";
import assert from "assert";
import BN from "bn.js";
import { PriceMath, increaseLiquidityQuoteByLiquidityWithParams } from "../../../../src";
import {
getTokenAFromLiquidity,
getTokenBFromLiquidity,
} from "../../../../src/utils/position-util";
const variations = [
[0, Percentage.fromFraction(1, 1000), new BN(17733543)] as const,
[0, Percentage.fromFraction(1, 100), new BN(17733543)] as const,
[0, Percentage.fromFraction(5, 100), new BN(17733543)] as const,
[234653, Percentage.fromFraction(1, 1000), new BN(17733543)] as const,
[234653, Percentage.fromFraction(1, 100), new BN(17733543)] as const,
[234653, Percentage.fromFraction(5, 100), new BN(17733543)] as const,
[-234653, Percentage.fromFraction(1, 1000), new BN(17733543)] as const,
[-234653, Percentage.fromFraction(1, 100), new BN(17733543)] as const,
[-234653, Percentage.fromFraction(5, 100), new BN(17733543)] as const,
];
function getTestSlipageRange(currIndex: number, slippage: Percentage) {
const sqrtPrice = PriceMath.tickIndexToSqrtPriceX64(currIndex);
const {
lowerBound: [_sLowerSqrtPrice, sLowerIndex],
upperBound: [_sUpperSqrtPrice, sUpperIndex],
} = PriceMath.getSlippageBoundForSqrtPrice(sqrtPrice, slippage);
return {
tickLowerIndex: sLowerIndex === sUpperIndex ? sLowerIndex - 1 : sLowerIndex,
tickUpperIndex: sUpperIndex,
tickCurrentIndex: currIndex,
};
}
// [---P---] = P is the current price & [] is the slippage boundary
// |-------| = Position Boundary
variations.forEach(([currentTickIndex, slippage, liquidity]) => {
describe("increaseLiquidityQuoteByLiquidity", () => {
it(`|[--------P--------]| @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex,
pTickUpperIndex: slippageRange.tickCurrentIndex,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`|----------------| [---P---] @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex - 200,
pTickUpperIndex: slippageRange.tickLowerIndex - 100,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`|--------------[--|--P----] @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex - 200,
pTickUpperIndex: slippageRange.tickLowerIndex + 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`[|---|---P------] @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex,
pTickUpperIndex: slippageRange.tickCurrentIndex - 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`[--|---|--P-------] @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex + 5,
pTickUpperIndex: slippageRange.tickCurrentIndex - 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`|-----[---P---]-----| @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex - 200,
pTickUpperIndex: slippageRange.tickUpperIndex + 200,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`[--|----P----]-----| @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex + 5,
pTickUpperIndex: slippageRange.tickUpperIndex + 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`|--[---P---|-----] @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex - 125,
pTickUpperIndex: slippageRange.tickCurrentIndex + 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`[---|---P---|----] @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickLowerIndex + 5,
pTickUpperIndex: slippageRange.tickCurrentIndex + 5,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`[---P---] |---------| @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickUpperIndex + 100,
pTickUpperIndex: slippageRange.tickUpperIndex + 200,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`[---P--|---]------| @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickCurrentIndex + 5,
pTickUpperIndex: slippageRange.tickUpperIndex + 100,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`[-----P--|---|] @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickCurrentIndex + 5,
pTickUpperIndex: slippageRange.tickUpperIndex,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
it(`[-------P--|---|--] @ tickCurrentIndex - ${currentTickIndex}, slippage - ${slippage.toDecimal()}%`, async () => {
const slippageRange = getTestSlipageRange(currentTickIndex, slippage);
testVariation({
pTickLowerIndex: slippageRange.tickCurrentIndex + 2,
pTickUpperIndex: slippageRange.tickUpperIndex - 2,
tickCurrentIndex: slippageRange.tickCurrentIndex,
liquidity,
slippageTolerance: slippage,
});
});
async function testVariation(params: {
pTickLowerIndex: number;
pTickUpperIndex: number;
tickCurrentIndex: number;
liquidity: BN;
slippageTolerance: Percentage;
}) {
const { pTickLowerIndex, pTickUpperIndex, tickCurrentIndex, liquidity } = params;
const sqrtPrice = PriceMath.tickIndexToSqrtPriceX64(tickCurrentIndex);
const quote = increaseLiquidityQuoteByLiquidityWithParams({
liquidity,
sqrtPrice,
tickLowerIndex: pTickLowerIndex,
tickUpperIndex: pTickUpperIndex,
tickCurrentIndex,
slippageTolerance: params.slippageTolerance,
});
const {
lowerBound: [sLowerSqrtPrice, sLowerIndex],
upperBound: [sUpperSqrtPrice, sUpperIndex],
} = PriceMath.getSlippageBoundForSqrtPrice(sqrtPrice, slippage);
const upperTokenEstA = getTokenEstA({
liquidity,
sqrtPrice: sUpperSqrtPrice,
currentTickIndex: sUpperIndex,
lowerTickIndex: pTickLowerIndex,
upperTickIndex: pTickUpperIndex,
});
const upperTokenEstB = getTokenEstB({
liquidity,
sqrtPrice: sUpperSqrtPrice,
currentTickIndex: sUpperIndex,
lowerTickIndex: pTickLowerIndex,
upperTickIndex: pTickUpperIndex,
});
const lowerTokenEstA = getTokenEstA({
liquidity,
sqrtPrice: sLowerSqrtPrice,
currentTickIndex: sLowerIndex,
lowerTickIndex: pTickLowerIndex,
upperTickIndex: pTickUpperIndex,
});
const lowerTokenEstB = getTokenEstB({
liquidity,
sqrtPrice: sLowerSqrtPrice,
currentTickIndex: sLowerIndex,
lowerTickIndex: pTickLowerIndex,
upperTickIndex: pTickUpperIndex,
});
const expectedTokenMaxA = BN.max(
BN.max(quote.tokenEstA, upperTokenEstA),
lowerTokenEstA,
);
const expectedTokenMaxB = BN.max(
BN.max(quote.tokenEstB, upperTokenEstB),
lowerTokenEstB,
);
// Generate expectations for TokenEstA and TokenEstB
const expectedTokenEstA = getTokenEstA({
liquidity,
sqrtPrice,
currentTickIndex: tickCurrentIndex,
lowerTickIndex: pTickLowerIndex,
upperTickIndex: pTickUpperIndex,
});
const expectedTokenEstB = getTokenEstB({
liquidity,
sqrtPrice,
currentTickIndex: tickCurrentIndex,
lowerTickIndex: pTickLowerIndex,
upperTickIndex: pTickUpperIndex,
});
assert.ok(
quote.tokenEstA.eq(expectedTokenEstA),
`tokenEstA: ${quote.tokenEstA.toString()} !== ${expectedTokenEstA.toString()}`,
);
assert.ok(
quote.tokenEstB.eq(expectedTokenEstB),
`tokenEstB: ${quote.tokenEstB.toString()} !== ${expectedTokenEstB.toString()}`,
);
assert.ok(
quote.tokenMaxA.eq(expectedTokenMaxA),
`tokenMaxA: ${quote.tokenMaxA.toString()} !== ${expectedTokenMaxA.toString()}`,
);
assert.ok(
quote.tokenMaxB.eq(expectedTokenMaxB),
`tokenMaxB: ${quote.tokenMaxB.toString()} !== ${expectedTokenMaxB.toString()}`,
);
}
});
});
function getTokenEstA(params: {
liquidity: BN;
sqrtPrice: BN;
currentTickIndex: number;
lowerTickIndex: number;
upperTickIndex: number;
}) {
const { liquidity, sqrtPrice, currentTickIndex, lowerTickIndex, upperTickIndex } = params;
const lowerSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(lowerTickIndex);
const upperSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(upperTickIndex);
if (currentTickIndex >= upperTickIndex) {
return ZERO;
}
if (currentTickIndex < lowerTickIndex) {
return getTokenAFromLiquidity(liquidity, lowerSqrtPrice, upperSqrtPrice, true);
}
return getTokenAFromLiquidity(liquidity, sqrtPrice, upperSqrtPrice, true);
}
function getTokenEstB(params: {
liquidity: BN;
sqrtPrice: BN;
currentTickIndex: number;
lowerTickIndex: number;
upperTickIndex: number;
}) {
const { liquidity, sqrtPrice, currentTickIndex, lowerTickIndex, upperTickIndex } = params;
const lowerSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(lowerTickIndex);
const upperSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(upperTickIndex);
if (currentTickIndex < lowerTickIndex) {
return ZERO;
}
if (currentTickIndex >= upperTickIndex) {
return getTokenBFromLiquidity(liquidity, lowerSqrtPrice, upperSqrtPrice, true);
}
return getTokenBFromLiquidity(liquidity, lowerSqrtPrice, sqrtPrice, true);
}

View File

@ -1,135 +0,0 @@
import * as assert from "assert";
import { PriceMath, increaseLiquidityQuoteByInputTokenWithParams } from "../../../../src";
import { BN } from "bn.js";
import { PublicKey } from "@solana/web3.js";
import { Percentage } from "@orca-so/common-sdk";
describe("edge cases", () => {
const tokenMintA = new PublicKey("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE");
const tokenMintB = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
it("sqrtPrice on lower bound, tokenB input", async () => {
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintB,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(0),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 0,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.isZero());
assert.ok(quote.tokenEstA.isZero());
assert.ok(quote.tokenEstB.isZero());
assert.ok(quote.tokenMaxA.isZero());
assert.ok(quote.tokenMaxB.isZero());
});
it("sqrtPrice on lower bound, tokenA input", async () => {
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintA,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(0),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 0,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.gtn(0));
assert.ok(quote.tokenEstA.gtn(0));
assert.ok(quote.tokenEstB.isZero());
assert.ok(quote.tokenMaxA.gtn(0));
assert.ok(quote.tokenMaxB.isZero());
});
it("tickCurrentIndex on lower bound but sqrtPrice not on lower bound, tokenA input", async () => {
assert.ok(PriceMath.tickIndexToSqrtPriceX64(1).subn(1).gt(PriceMath.tickIndexToSqrtPriceX64(0)));
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintA,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(1).subn(1),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 0,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.gtn(0));
assert.ok(quote.tokenEstA.gtn(0));
assert.ok(quote.tokenEstB.gtn(0));
assert.ok(quote.tokenMaxA.gtn(0));
assert.ok(quote.tokenMaxB.gtn(0));
});
it("tickCurrentIndex on lower bound but sqrtPrice not on lower bound, tokenB input", async () => {
assert.ok(PriceMath.tickIndexToSqrtPriceX64(1).subn(1).gt(PriceMath.tickIndexToSqrtPriceX64(0)));
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintB,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(1).subn(1),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 0,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.gtn(0));
assert.ok(quote.tokenEstA.gtn(0));
assert.ok(quote.tokenEstB.gtn(0));
assert.ok(quote.tokenMaxA.gtn(0));
assert.ok(quote.tokenMaxB.gtn(0));
});
it("sqrtPrice on upper bound, tokenA input", async () => {
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintA,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(64),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 64,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.isZero());
assert.ok(quote.tokenEstA.isZero());
assert.ok(quote.tokenEstB.isZero());
assert.ok(quote.tokenMaxA.isZero());
assert.ok(quote.tokenMaxB.isZero());
});
it("sqrtPrice on upper bound, tokenB input", async () => {
const quote = increaseLiquidityQuoteByInputTokenWithParams({
inputTokenAmount: new BN(1000),
inputTokenMint: tokenMintB,
sqrtPrice: PriceMath.tickIndexToSqrtPriceX64(64),
tokenMintA,
tokenMintB,
tickLowerIndex: 0,
tickUpperIndex: 64,
tickCurrentIndex: 64,
slippageTolerance: Percentage.fromFraction(0, 100),
});
assert.ok(quote.liquidityAmount.gtn(0));
assert.ok(quote.tokenEstA.isZero());
assert.ok(quote.tokenEstB.gtn(0));
assert.ok(quote.tokenMaxA.isZero());
assert.ok(quote.tokenMaxB.gtn(0));
});
});

View File

@ -0,0 +1,126 @@
import { MathUtil, Percentage } from "@orca-so/common-sdk";
import assert from "assert";
import BN from "bn.js";
import Decimal from "decimal.js";
import {
MAX_SQRT_PRICE_BN,
MAX_TICK_INDEX,
MIN_SQRT_PRICE_BN,
MIN_TICK_INDEX,
PriceMath,
} from "../../../../src";
function toSqrtPrice(n: number) {
return PriceMath.priceToSqrtPriceX64(new Decimal(n), 6, 6);
}
const EVAL_PRECISION = 16;
const variations = [
[MAX_SQRT_PRICE_BN, Percentage.fromFraction(0, 100), true] as const,
[MAX_SQRT_PRICE_BN, Percentage.fromFraction(1, 1000), true] as const,
[MAX_SQRT_PRICE_BN, Percentage.fromFraction(1, 100), true] as const,
[MIN_SQRT_PRICE_BN, Percentage.fromFraction(0, 1000), true] as const,
[MIN_SQRT_PRICE_BN, Percentage.fromFraction(1, 1000), true] as const,
[MIN_SQRT_PRICE_BN, Percentage.fromFraction(1, 100), true] as const,
[MIN_SQRT_PRICE_BN, Percentage.fromFraction(1, 100), true] as const,
[toSqrtPrice(5), Percentage.fromFraction(0, 1000), false] as const,
[toSqrtPrice(5), Percentage.fromFraction(1, 1000), false] as const,
[toSqrtPrice(5), Percentage.fromFraction(10, 1000), false] as const,
[toSqrtPrice(1000000), Percentage.fromFraction(0, 1000), false] as const,
[toSqrtPrice(1000000), Percentage.fromFraction(5, 1000), false] as const,
[toSqrtPrice(1000000), Percentage.fromFraction(20, 1000), false] as const,
[toSqrtPrice(61235.33), Percentage.fromFraction(0, 1000), false] as const,
[toSqrtPrice(61235.33), Percentage.fromFraction(5, 1000), false] as const,
[toSqrtPrice(61235.33), Percentage.fromFraction(20, 1000), false] as const,
];
function toPrecisionLevel(decimal: Decimal) {
return decimal.toSignificantDigits(EVAL_PRECISION);
}
variations.forEach(([sqrtPrice, slippage, ignorePrecisionVerification]) => {
describe("PriceMath - getSlippageBoundForSqrtPrice tests", () => {
it(`slippage boundary for sqrt price - ${sqrtPrice.toString()}, slippage - ${slippage
.toDecimal()
.mul(100)}%`, () => {
const { lowerBound, upperBound } = PriceMath.getSlippageBoundForSqrtPrice(
sqrtPrice,
slippage,
);
const price = PriceMath.sqrtPriceX64ToPrice(sqrtPrice, 6, 6);
const slippageDecimal = slippage.toDecimal();
const expectedUpperSlippagePrice = toPrecisionLevel(price.mul(slippageDecimal.add(1)));
const expectedLowerSlippagePrice = toPrecisionLevel(
price.mul(new Decimal(1).sub(slippageDecimal)),
);
const expectedUpperSqrtPrice = BN.min(
BN.max(MathUtil.toX64(expectedUpperSlippagePrice.sqrt()), MIN_SQRT_PRICE_BN),
MAX_SQRT_PRICE_BN,
);
const expectedLowerSqrtPrice = BN.min(
BN.max(MathUtil.toX64(expectedLowerSlippagePrice.sqrt()), MIN_SQRT_PRICE_BN),
MAX_SQRT_PRICE_BN,
);
const expectedUpperTickIndex = PriceMath.sqrtPriceX64ToTickIndex(expectedUpperSqrtPrice);
const expectedLowerTickIndex = PriceMath.sqrtPriceX64ToTickIndex(expectedLowerSqrtPrice);
const lowerBoundSqrtPrice = lowerBound[0];
const lowerBoundTickIndex = lowerBound[1];
const lowerBoundPrice = toPrecisionLevel(
PriceMath.sqrtPriceX64ToPrice(lowerBoundSqrtPrice, 6, 6),
);
const upperBoundSqrtPrice = upperBound[0];
const upperBoundTickIndex = upperBound[1];
const upperBoundPrice = toPrecisionLevel(
PriceMath.sqrtPriceX64ToPrice(upperBoundSqrtPrice, 6, 6),
);
// For larger sqrt-price boundary values, it's difficult to verify exactly due to the precision loss.
// We will only verify that it won't crash and the upper and lower bounds are within the expected range by
// testing that the function won't crash.
if (!ignorePrecisionVerification) {
assert.ok(
lowerBoundPrice.eq(expectedLowerSlippagePrice),
`lower slippage price ${lowerBoundPrice.toString()} should equal ${expectedLowerSlippagePrice.toString()}`,
);
assert.ok(
upperBoundPrice.eq(expectedUpperSlippagePrice),
`upper slippage price ${upperBoundPrice.toString()} should equal ${expectedUpperSlippagePrice.toString()}`,
);
assert.ok(
expectedUpperTickIndex === upperBoundTickIndex,
`upper tick index ${upperBoundTickIndex} should equal ${expectedUpperTickIndex}`,
);
assert.ok(
expectedLowerTickIndex === lowerBoundTickIndex,
`lower tick index ${lowerBoundTickIndex} should equal ${expectedLowerTickIndex}`,
);
} else {
// Verify generally the conditions hold between sqrtPrices and tick indicies
assert.ok(
lowerBoundSqrtPrice.gte(MIN_SQRT_PRICE_BN) && lowerBoundSqrtPrice.lte(MAX_SQRT_PRICE_BN),
`lower bound sqrt price ${lowerBoundSqrtPrice.toString()} should be within bounds of MIN_SQRT_PRICE_BN & MAX_SQRT_PRICE_BN`,
);
assert.ok(
upperBoundSqrtPrice.gte(MIN_SQRT_PRICE_BN) && upperBoundSqrtPrice.lte(MAX_SQRT_PRICE_BN),
`lower bound sqrt price ${upperBoundSqrtPrice.toString()} should be within bounds of MIN_SQRT_PRICE_BN & MAX_SQRT_PRICE_BN`,
);
assert.ok(
lowerBoundTickIndex >= MIN_TICK_INDEX && lowerBoundTickIndex <= MAX_TICK_INDEX,
`lower bound tick index ${lowerBoundTickIndex} should be within bounds of MIN_TICK_INDEX & MAX_TICK_INDEX`,
);
assert.ok(
upperBoundTickIndex >= MIN_TICK_INDEX && upperBoundTickIndex <= MAX_TICK_INDEX,
`upper bound tick index ${upperBoundTickIndex} should be within bounds of MIN_TICK_INDEX & MAX_TICK_INDEX`,
);
}
});
});
});

View File

@ -12,7 +12,7 @@ import {
collectFeesQuote,
collectRewardsQuote,
decreaseLiquidityQuoteByLiquidity,
increaseLiquidityQuoteByInputToken,
increaseLiquidityQuoteByInputTokenUsingPriceSlippage,
toTx
} from "../../../src";
import { WhirlpoolContext } from "../../../src/context";
@ -84,7 +84,7 @@ describe("whirlpool-impl", () => {
);
const inputTokenMint = poolData.tokenMintA;
const quote = increaseLiquidityQuoteByInputToken(
const quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage(
inputTokenMint,
new Decimal(50),
tickLower,
@ -193,7 +193,7 @@ describe("whirlpool-impl", () => {
const inputTokenMint = poolData.tokenMintA;
const depositAmount = new Decimal(50);
const quote = increaseLiquidityQuoteByInputToken(
const quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage(
inputTokenMint,
depositAmount,
tickLower,

View File

@ -14,7 +14,7 @@ import {
PoolUtil,
PriceMath,
Whirlpool,
increaseLiquidityQuoteByInputToken
increaseLiquidityQuoteByInputTokenUsingPriceSlippage,
} from "../../src";
import { WhirlpoolContext } from "../../src/context";
@ -215,7 +215,7 @@ export async function initPosition(
tokenBDecimal,
tickSpacing
);
const quote = await increaseLiquidityQuoteByInputToken(
const quote = await increaseLiquidityQuoteByInputTokenUsingPriceSlippage(
inputTokenMint,
new Decimal(inputTokenAmount),
lowerTick,