From 8a7ed57bc5680e28cb5cb8274d76f15261f59d16 Mon Sep 17 00:00:00 2001 From: yugure-orca <109891005+yugure-orca@users.noreply.github.com> Date: Thu, 24 Nov 2022 15:13:59 +0900 Subject: [PATCH] fix: strict initialized checking at swapQuoteWithParams (#55) * fix: strict initialized checking at swapQuoteWithParams - removed checkIfAllTickArraysInitialized from swapQuoteWithParams - added check to ensure that all TickArrays are initialized at getSwapTx - In a TickArraySequence, uninitialized TickArrays are truncated. - fix sqrtPriceLimit range check - fix commitment issue in test code - remove test "swap with a manual quote with dev-fee of 200%" (the code does not throw the exception) * refactor: TickArraySequence - remove Non-null assertion operator(!) - use simple foreach loop and early break - avoid name ambiguity (tickArrays to sequence) - set constructor argument (tickArrays) to readonly passed all tests (integration & sdk) --- sdk/src/impl/whirlpool-impl.ts | 12 + sdk/src/quotes/public/swap-quote.ts | 3 - sdk/src/quotes/swap/swap-quote-impl.ts | 2 +- sdk/src/quotes/swap/swap-quote-utils.ts | 14 - sdk/src/quotes/swap/tick-array-sequence.ts | 33 +- .../sdk/whirlpools/swap/swap-array.test.ts | 318 +++++++++++++++++- .../sdk/whirlpools/swap/swap-dev-fee.test.ts | 47 --- .../sdk/whirlpools/swap/swap-traverse.test.ts | 108 ++++++ .../sdk/whirlpools/whirlpool-impl.test.ts | 5 +- 9 files changed, 461 insertions(+), 81 deletions(-) delete mode 100644 sdk/src/quotes/swap/swap-quote-utils.ts diff --git a/sdk/src/impl/whirlpool-impl.ts b/sdk/src/impl/whirlpool-impl.ts index be098e9..7eb0ed2 100644 --- a/sdk/src/impl/whirlpool-impl.ts +++ b/sdk/src/impl/whirlpool-impl.ts @@ -435,6 +435,18 @@ export class WhirlpoolImpl implements Whirlpool { initTxBuilder?: TransactionBuilder ): Promise { invariant(input.amount.gt(ZERO), "swap amount must be more than zero."); + + // Check if all the tick arrays have been initialized. + const tickArrayAddresses = [input.tickArray0, input.tickArray1, input.tickArray2]; + const tickArrays = await this.fetcher.listTickArrays(tickArrayAddresses, true); + const uninitializedIndices = TickArrayUtil.getUninitializedArrays(tickArrays); + if (uninitializedIndices.length > 0) { + const uninitializedArrays = uninitializedIndices + .map((index) => tickArrayAddresses[index].toBase58()) + .join(", "); + throw new Error(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`); + } + const { amount, aToB } = input; const whirlpool = this.data; const txBuilder = diff --git a/sdk/src/quotes/public/swap-quote.ts b/sdk/src/quotes/public/swap-quote.ts index 9591340..9ca2fff 100644 --- a/sdk/src/quotes/public/swap-quote.ts +++ b/sdk/src/quotes/public/swap-quote.ts @@ -9,7 +9,6 @@ import { PoolUtil, TokenType } from "../../utils/public"; import { SwapUtils } from "../../utils/public/swap-utils"; import { Whirlpool } from "../../whirlpool-client"; import { simulateSwap } from "../swap/swap-quote-impl"; -import { checkIfAllTickArraysInitialized } from "../swap/swap-quote-utils"; import { DevFeeSwapQuote } from "./dev-fee-swap-quote"; /** @@ -143,8 +142,6 @@ export function swapQuoteWithParams( params: SwapQuoteParam, slippageTolerance: Percentage ): SwapQuote { - checkIfAllTickArraysInitialized(params.tickArrays); - const quote = simulateSwap(params); const slippageAdjustedQuote: SwapQuote = { diff --git a/sdk/src/quotes/swap/swap-quote-impl.ts b/sdk/src/quotes/swap/swap-quote-impl.ts index da4294c..bae8f09 100644 --- a/sdk/src/quotes/swap/swap-quote-impl.ts +++ b/sdk/src/quotes/swap/swap-quote-impl.ts @@ -23,7 +23,7 @@ export function simulateSwap(params: SwapQuoteParam): SwapQuote { amountSpecifiedIsInput, } = params; - if (sqrtPriceLimit.gt(new BN(MAX_SQRT_PRICE) || sqrtPriceLimit.lt(new BN(MIN_SQRT_PRICE)))) { + if (sqrtPriceLimit.gt(new BN(MAX_SQRT_PRICE)) || sqrtPriceLimit.lt(new BN(MIN_SQRT_PRICE))) { throw new WhirlpoolsError( "Provided SqrtPriceLimit is out of bounds.", SwapErrorCode.SqrtPriceOutOfBounds diff --git a/sdk/src/quotes/swap/swap-quote-utils.ts b/sdk/src/quotes/swap/swap-quote-utils.ts deleted file mode 100644 index ea91f60..0000000 --- a/sdk/src/quotes/swap/swap-quote-utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TickArray, TickArrayUtil } from "../.."; - -export function checkIfAllTickArraysInitialized(tickArrays: TickArray[]) { - // Check if all the tick arrays have been initialized. - const uninitializedIndices = TickArrayUtil.getUninitializedArrays( - tickArrays.map((array) => array.data) - ); - if (uninitializedIndices.length > 0) { - const uninitializedArrays = uninitializedIndices - .map((index) => tickArrays[index].address.toBase58()) - .join(", "); - throw new Error(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`); - } -} diff --git a/sdk/src/quotes/swap/tick-array-sequence.ts b/sdk/src/quotes/swap/tick-array-sequence.ts index 2ea6084..d3ad665 100644 --- a/sdk/src/quotes/swap/tick-array-sequence.ts +++ b/sdk/src/quotes/swap/tick-array-sequence.ts @@ -3,22 +3,29 @@ import { MAX_TICK_INDEX, MIN_TICK_INDEX, TickArray, + TickArrayData, TickData, TICK_ARRAY_SIZE, } from "../../types/public"; import { TickArrayIndex } from "./tick-array-index"; import { PublicKey } from "@solana/web3.js"; +type InitializedTickArray = TickArray & { + // override + data: TickArrayData; +}; + /** * NOTE: differs from contract method of having the swap manager keep track of array index. * This is due to the initial requirement to lazy load tick-arrays. This requirement is no longer necessary. */ export class TickArraySequence { + private sequence: InitializedTickArray[]; private touchedArrays: boolean[]; private startArrayIndex: number; constructor( - readonly tickArrays: TickArray[], + tickArrays: Readonly, readonly tickSpacing: number, readonly aToB: boolean ) { @@ -26,15 +33,27 @@ export class TickArraySequence { throw new Error("TickArray index 0 must be initialized"); } - this.touchedArrays = [...Array(tickArrays.length).fill(false)]; + // If an uninitialized TickArray appears, truncate all TickArrays after it (inclusive). + this.sequence = []; + for (const tickArray of tickArrays) { + if ( !tickArray || !tickArray.data ) { + break; + } + this.sequence.push({ + address: tickArray.address, + data: tickArray.data + }); + } + + this.touchedArrays = [...Array(this.sequence.length).fill(false)]; this.startArrayIndex = TickArrayIndex.fromTickIndex( - tickArrays[0].data.startTickIndex, + this.sequence[0].data.startTickIndex, this.tickSpacing ).arrayIndex; } checkArrayContainsTickIndex(sequenceIndex: number, tickIndex: number) { - const tickArray = this.tickArrays[sequenceIndex]?.data; + const tickArray = this.sequence[sequenceIndex]?.data; if (!tickArray) { return false; } @@ -48,7 +67,7 @@ export class TickArraySequence { getTouchedArrays(minArraySize: number): PublicKey[] { let result = this.touchedArrays.reduce((prev, curr, index) => { if (curr) { - prev.push(this.tickArrays[index].address); + prev.push(this.sequence[index].address); } return prev; }, []); @@ -77,7 +96,7 @@ export class TickArraySequence { } const localArrayIndex = this.getLocalArrayIndex(targetTaIndex.arrayIndex, this.aToB); - const tickArray = this.tickArrays[localArrayIndex].data; + const tickArray = this.sequence[localArrayIndex].data; this.touchedArrays[localArrayIndex] = true; @@ -148,7 +167,7 @@ export class TickArraySequence { private isArrayIndexInBounds(index: TickArrayIndex, aToB: boolean) { // a+0...a+n-1 array index is ok const localArrayIndex = this.getLocalArrayIndex(index.arrayIndex, aToB); - const seqLength = this.tickArrays.length; + const seqLength = this.sequence.length; return localArrayIndex >= 0 && localArrayIndex < seqLength; } diff --git a/sdk/tests/sdk/whirlpools/swap/swap-array.test.ts b/sdk/tests/sdk/whirlpools/swap/swap-array.test.ts index cd15909..69016e7 100644 --- a/sdk/tests/sdk/whirlpools/swap/swap-array.test.ts +++ b/sdk/tests/sdk/whirlpools/swap/swap-array.test.ts @@ -34,7 +34,57 @@ describe("swap arrays test", () => { const slippageTolerance = Percentage.fromFraction(0, 100); /** - * |-------------c2-----|xxxxxxxxxxxxxxxxx|------c1-----------| + * |--------------------|xxxxxxxxxxxxxxxxx|-c2---c1-----------| + */ + it("3 sequential arrays, 2nd array not initialized, use tickArray0 only, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new u64(10000); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + tradeAmount, + slippageTolerance, + ctx.program.programId, + fetcher, + true + ); + + // Verify with an actual swap. + // estimatedEndTickIndex is 8446 (arrayIndex: 1) + assert.equal(quote.aToB, true); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(true).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + + /** + * |--------------------|xxxxxxxxxxxxxc2xx|------c1-----------| */ it("3 sequential arrays, 2nd array not initialized, a->b", async () => { const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing); @@ -55,14 +105,14 @@ describe("swap arrays test", () => { ], }); + // estimatedEndTickIndex is 4091 (arrayIndex: 0 (not initialized)) const whirlpoolData = await whirlpool.refreshData(); - const missingTickArray = PDAUtil.getTickArray(ctx.program.programId, whirlpool.getAddress(), 0); - const expectedError = `[${missingTickArray.publicKey.toBase58()}] need to be initialized`; + const expectedError = "Swap input value traversed too many arrays."; await assert.rejects( swapQuoteByInputToken( whirlpool, whirlpoolData.tokenMintA, - new u64(10000), + new u64(40_000_000), slippageTolerance, ctx.program.programId, fetcher, @@ -71,9 +121,59 @@ describe("swap arrays test", () => { (err: Error) => err.message.indexOf(expectedError) != -1 ); }); + + /** + * |-------------c1--c2-|xxxxxxxxxxxxxxxxx|-------------------| + */ + it("3 sequential arrays, 2nd array not initialized, use tickArray0 only, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new u64(10000); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + tradeAmount, + slippageTolerance, + ctx.program.programId, + fetcher, + true + ); + + // Verify with an actual swap. + // estimatedEndTickIndex is -2816 (arrayIndex: -1) + assert.equal(quote.aToB, false); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(false).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); /** - * |-------------c1-----|xxxxxxxxxxxxxxxxx|------c2-----------| + * |-------------c1-----|xxc2xxxxxxxxxxxxx|-------------------| */ it("3 sequential arrays, 2nd array not initialized, b->a", async () => { const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing); @@ -94,14 +194,14 @@ describe("swap arrays test", () => { ], }); + // estimatedEndTickIndex is 556 (arrayIndex: 0 (not initialized)) const whirlpoolData = await whirlpool.refreshData(); - const missingTickArray = PDAUtil.getTickArray(ctx.program.programId, whirlpool.getAddress(), 0); - const expectedError = `[${missingTickArray.publicKey.toBase58()}] need to be initialized`; + const expectedError = "Swap input value traversed too many arrays."; await assert.rejects( swapQuoteByInputToken( whirlpool, whirlpoolData.tokenMintB, - new u64(10000), + new u64(40_000_000), slippageTolerance, ctx.program.programId, fetcher, @@ -111,6 +211,106 @@ describe("swap arrays test", () => { ); }); + /** + * |xxxxxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxx|-c2---c1-----------| + */ + it("3 sequential arrays, 2nd array and 3rd array not initialized, use tickArray0 only, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new u64(10000); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + tradeAmount, + slippageTolerance, + ctx.program.programId, + fetcher, + true + ); + + // Verify with an actual swap. + // estimatedEndTickIndex is 8446 (arrayIndex: 1) + assert.equal(quote.aToB, true); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(true).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + + /** + * |-------------c1--c2-|xxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxxxx| + */ + it("3 sequential arrays, 2nd array and 3rd array not initialized, use tickArray0 only, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new u64(10000); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + tradeAmount, + slippageTolerance, + ctx.program.programId, + fetcher, + true + ); + + // Verify with an actual swap. + // estimatedEndTickIndex is -2816 (arrayIndex: -1) + assert.equal(quote.aToB, false); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(false).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + /** * c1|------------------|-----------------|-------------------| */ @@ -536,4 +736,106 @@ describe("swap arrays test", () => { assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); }); + + /** + * |xxxxxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxx|-c2---c1-----------| + */ + it("Whirlpool.swap with uninitialized TickArrays, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new u64(10000); + const aToB = true; + const tickArrays = SwapUtils.getTickArrayPublicKeys( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress() + ); + + await assert.rejects( + whirlpool.swap({ + amount: tradeAmount, + amountSpecifiedIsInput: true, + aToB, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + tickArray0: tickArrays[0], + tickArray1: tickArrays[1], + tickArray2: tickArrays[2], + }), + (err: Error) => { + const uninitializedArrays = [tickArrays[1].toBase58(), tickArrays[2].toBase58()].join(", "); + return err.message.indexOf(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`) >= 0; + } + ); + }); + + /** + * |-------------c1--c2-|xxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxxxx| + */ + it("Whirlpool.swap with uninitialized TickArrays, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new u64(10000); + const aToB = false; + const tickArrays = SwapUtils.getTickArrayPublicKeys( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress() + ); + + await assert.rejects( + whirlpool.swap({ + amount: tradeAmount, + amountSpecifiedIsInput: true, + aToB, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + tickArray0: tickArrays[0], + tickArray1: tickArrays[1], + tickArray2: tickArrays[2], + }), + (err: Error) => { + const uninitializedArrays = [tickArrays[1].toBase58(), tickArrays[2].toBase58()].join(", "); + return err.message.indexOf(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`) >= 0; + } + ); + }); }); diff --git a/sdk/tests/sdk/whirlpools/swap/swap-dev-fee.test.ts b/sdk/tests/sdk/whirlpools/swap/swap-dev-fee.test.ts index 7ff372f..1a1a612 100644 --- a/sdk/tests/sdk/whirlpools/swap/swap-dev-fee.test.ts +++ b/sdk/tests/sdk/whirlpools/swap/swap-dev-fee.test.ts @@ -428,53 +428,6 @@ describe("whirlpool-dev-fee-swap", () => { (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.InvalidDevFeePercentage ); }); - - it("swap with a manual quote with dev-fee of 200%", async () => { - const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); - const devWallet = Keypair.generate(); - const whirlpool = await setupSwapTest({ - ctx, - client, - tickSpacing, - initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), - initArrayStartTicks: [-5632, 0, 5632], - fundedPositions: [ - buildPosition( - // a - { arrayIndex: -1, offsetIndex: 10 }, - { arrayIndex: 1, offsetIndex: 23 }, - tickSpacing, - new anchor.BN(250_000_000) - ), - ], - }); - - const devFeePercentage = Percentage.fromFraction(200, 100); // 200% - const inputTokenAmount = new u64(119500000); - const whirlpoolData = await whirlpool.refreshData(); - const swapToken = whirlpoolData.tokenMintB; - - assert.rejects( - async () => - ( - await whirlpool.swapWithDevFees( - { - amount: new u64(10000), - devFeeAmount: new u64(30000), - amountSpecifiedIsInput: true, - aToB: true, - otherAmountThreshold: ZERO, - sqrtPriceLimit: ZERO, - tickArray0: PublicKey.default, - tickArray1: PublicKey.default, - tickArray2: PublicKey.default, - }, - devWallet.publicKey - ) - ).buildAndExecute(), - (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.InvalidDevFeePercentage - ); - }); }); async function getQuotes( diff --git a/sdk/tests/sdk/whirlpools/swap/swap-traverse.test.ts b/sdk/tests/sdk/whirlpools/swap/swap-traverse.test.ts index 6890332..f61d13f 100644 --- a/sdk/tests/sdk/whirlpools/swap/swap-traverse.test.ts +++ b/sdk/tests/sdk/whirlpools/swap/swap-traverse.test.ts @@ -7,11 +7,15 @@ import { buildWhirlpoolClient, MAX_TICK_INDEX, MIN_TICK_INDEX, + MAX_SQRT_PRICE, + MIN_SQRT_PRICE, PriceMath, swapQuoteByInputToken, swapQuoteByOutputToken, + swapQuoteWithParams, TICK_ARRAY_SIZE, WhirlpoolContext, + SwapUtils, } from "../../../../src"; import { SwapErrorCode, WhirlpoolsError } from "../../../../src/errors/errors"; import { assertInputOutputQuoteEqual, assertQuoteAndResults, TickSpacing } from "../../../utils"; @@ -1165,4 +1169,108 @@ describe("swap traversal tests", () => { const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); assertQuoteAndResults(aToB, quote, newData, beforeVaultAmounts, afterVaultAmounts); }); + + /** + * sqrtPriceLimit < MIN_SQRT_PRICE + * |--------------------|-----------------|---------x1----------| + */ + it("3 arrays, sqrtPriceLimit is out of bounds (< MIN_SQRT_PRICE), a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 22 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = true; + const tickArrays = await SwapUtils.getTickArrays( + currIndex, + tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress(), + fetcher, + true + ); + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new u64("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: new BN(MIN_SQRT_PRICE).subn(1), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + }, + slippageTolerance + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.SqrtPriceOutOfBounds + ); + }); + + /** + * sqrtPriceLimit > MAX_SQRT_PRICE + * |-----x1-------------|-----------------|---------------------| + */ + it("3 arrays, sqrtPriceLimit is out of bounds (> MAX_SQRT_PRICE), b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = false; + const tickArrays = await SwapUtils.getTickArrays( + currIndex, + tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress(), + fetcher, + true + ); + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new u64("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: new BN(MAX_SQRT_PRICE).addn(1), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + }, + slippageTolerance + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.SqrtPriceOutOfBounds + ); + }); }); diff --git a/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts b/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts index 0e58d0a..9186289 100644 --- a/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts +++ b/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts @@ -23,7 +23,10 @@ import { initTestPool } from "../../utils/init-utils"; import { mintTokensToTestAccount } from "../../utils/test-builders"; describe("whirlpool-impl", () => { - const provider = anchor.AnchorProvider.local(); + // The default commitment of AnchorProvider is "processed". + // But commitment of some Token operations is based on “confirmed”, and preflight simulation sometimes fail. + // So use "confirmed" consistently. + const provider = anchor.AnchorProvider.local(undefined, {commitment: "confirmed", preflightCommitment: "confirmed"}); anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.Whirlpool; const ctx = WhirlpoolContext.fromWorkspace(provider, program);