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)
This commit is contained in:
parent
61874e5c56
commit
8a7ed57bc5
|
@ -435,6 +435,18 @@ export class WhirlpoolImpl implements Whirlpool {
|
||||||
initTxBuilder?: TransactionBuilder
|
initTxBuilder?: TransactionBuilder
|
||||||
): Promise<TransactionBuilder> {
|
): Promise<TransactionBuilder> {
|
||||||
invariant(input.amount.gt(ZERO), "swap amount must be more than zero.");
|
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 { amount, aToB } = input;
|
||||||
const whirlpool = this.data;
|
const whirlpool = this.data;
|
||||||
const txBuilder =
|
const txBuilder =
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { PoolUtil, TokenType } from "../../utils/public";
|
||||||
import { SwapUtils } from "../../utils/public/swap-utils";
|
import { SwapUtils } from "../../utils/public/swap-utils";
|
||||||
import { Whirlpool } from "../../whirlpool-client";
|
import { Whirlpool } from "../../whirlpool-client";
|
||||||
import { simulateSwap } from "../swap/swap-quote-impl";
|
import { simulateSwap } from "../swap/swap-quote-impl";
|
||||||
import { checkIfAllTickArraysInitialized } from "../swap/swap-quote-utils";
|
|
||||||
import { DevFeeSwapQuote } from "./dev-fee-swap-quote";
|
import { DevFeeSwapQuote } from "./dev-fee-swap-quote";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,8 +142,6 @@ export function swapQuoteWithParams(
|
||||||
params: SwapQuoteParam,
|
params: SwapQuoteParam,
|
||||||
slippageTolerance: Percentage
|
slippageTolerance: Percentage
|
||||||
): SwapQuote {
|
): SwapQuote {
|
||||||
checkIfAllTickArraysInitialized(params.tickArrays);
|
|
||||||
|
|
||||||
const quote = simulateSwap(params);
|
const quote = simulateSwap(params);
|
||||||
|
|
||||||
const slippageAdjustedQuote: SwapQuote = {
|
const slippageAdjustedQuote: SwapQuote = {
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function simulateSwap(params: SwapQuoteParam): SwapQuote {
|
||||||
amountSpecifiedIsInput,
|
amountSpecifiedIsInput,
|
||||||
} = params;
|
} = 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(
|
throw new WhirlpoolsError(
|
||||||
"Provided SqrtPriceLimit is out of bounds.",
|
"Provided SqrtPriceLimit is out of bounds.",
|
||||||
SwapErrorCode.SqrtPriceOutOfBounds
|
SwapErrorCode.SqrtPriceOutOfBounds
|
||||||
|
|
|
@ -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.`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,22 +3,29 @@ import {
|
||||||
MAX_TICK_INDEX,
|
MAX_TICK_INDEX,
|
||||||
MIN_TICK_INDEX,
|
MIN_TICK_INDEX,
|
||||||
TickArray,
|
TickArray,
|
||||||
|
TickArrayData,
|
||||||
TickData,
|
TickData,
|
||||||
TICK_ARRAY_SIZE,
|
TICK_ARRAY_SIZE,
|
||||||
} from "../../types/public";
|
} from "../../types/public";
|
||||||
import { TickArrayIndex } from "./tick-array-index";
|
import { TickArrayIndex } from "./tick-array-index";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
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.
|
* 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.
|
* This is due to the initial requirement to lazy load tick-arrays. This requirement is no longer necessary.
|
||||||
*/
|
*/
|
||||||
export class TickArraySequence {
|
export class TickArraySequence {
|
||||||
|
private sequence: InitializedTickArray[];
|
||||||
private touchedArrays: boolean[];
|
private touchedArrays: boolean[];
|
||||||
private startArrayIndex: number;
|
private startArrayIndex: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly tickArrays: TickArray[],
|
tickArrays: Readonly<TickArray[]>,
|
||||||
readonly tickSpacing: number,
|
readonly tickSpacing: number,
|
||||||
readonly aToB: boolean
|
readonly aToB: boolean
|
||||||
) {
|
) {
|
||||||
|
@ -26,15 +33,27 @@ export class TickArraySequence {
|
||||||
throw new Error("TickArray index 0 must be initialized");
|
throw new Error("TickArray index 0 must be initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.touchedArrays = [...Array<boolean>(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<boolean>(this.sequence.length).fill(false)];
|
||||||
this.startArrayIndex = TickArrayIndex.fromTickIndex(
|
this.startArrayIndex = TickArrayIndex.fromTickIndex(
|
||||||
tickArrays[0].data.startTickIndex,
|
this.sequence[0].data.startTickIndex,
|
||||||
this.tickSpacing
|
this.tickSpacing
|
||||||
).arrayIndex;
|
).arrayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkArrayContainsTickIndex(sequenceIndex: number, tickIndex: number) {
|
checkArrayContainsTickIndex(sequenceIndex: number, tickIndex: number) {
|
||||||
const tickArray = this.tickArrays[sequenceIndex]?.data;
|
const tickArray = this.sequence[sequenceIndex]?.data;
|
||||||
if (!tickArray) {
|
if (!tickArray) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -48,7 +67,7 @@ export class TickArraySequence {
|
||||||
getTouchedArrays(minArraySize: number): PublicKey[] {
|
getTouchedArrays(minArraySize: number): PublicKey[] {
|
||||||
let result = this.touchedArrays.reduce<PublicKey[]>((prev, curr, index) => {
|
let result = this.touchedArrays.reduce<PublicKey[]>((prev, curr, index) => {
|
||||||
if (curr) {
|
if (curr) {
|
||||||
prev.push(this.tickArrays[index].address);
|
prev.push(this.sequence[index].address);
|
||||||
}
|
}
|
||||||
return prev;
|
return prev;
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -77,7 +96,7 @@ export class TickArraySequence {
|
||||||
}
|
}
|
||||||
|
|
||||||
const localArrayIndex = this.getLocalArrayIndex(targetTaIndex.arrayIndex, this.aToB);
|
const localArrayIndex = this.getLocalArrayIndex(targetTaIndex.arrayIndex, this.aToB);
|
||||||
const tickArray = this.tickArrays[localArrayIndex].data;
|
const tickArray = this.sequence[localArrayIndex].data;
|
||||||
|
|
||||||
this.touchedArrays[localArrayIndex] = true;
|
this.touchedArrays[localArrayIndex] = true;
|
||||||
|
|
||||||
|
@ -148,7 +167,7 @@ export class TickArraySequence {
|
||||||
private isArrayIndexInBounds(index: TickArrayIndex, aToB: boolean) {
|
private isArrayIndexInBounds(index: TickArrayIndex, aToB: boolean) {
|
||||||
// a+0...a+n-1 array index is ok
|
// a+0...a+n-1 array index is ok
|
||||||
const localArrayIndex = this.getLocalArrayIndex(index.arrayIndex, aToB);
|
const localArrayIndex = this.getLocalArrayIndex(index.arrayIndex, aToB);
|
||||||
const seqLength = this.tickArrays.length;
|
const seqLength = this.sequence.length;
|
||||||
return localArrayIndex >= 0 && localArrayIndex < seqLength;
|
return localArrayIndex >= 0 && localArrayIndex < seqLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,57 @@ describe("swap arrays test", () => {
|
||||||
const slippageTolerance = Percentage.fromFraction(0, 100);
|
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 () => {
|
it("3 sequential arrays, 2nd array not initialized, a->b", async () => {
|
||||||
const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing);
|
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 whirlpoolData = await whirlpool.refreshData();
|
||||||
const missingTickArray = PDAUtil.getTickArray(ctx.program.programId, whirlpool.getAddress(), 0);
|
const expectedError = "Swap input value traversed too many arrays.";
|
||||||
const expectedError = `[${missingTickArray.publicKey.toBase58()}] need to be initialized`;
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
swapQuoteByInputToken(
|
swapQuoteByInputToken(
|
||||||
whirlpool,
|
whirlpool,
|
||||||
whirlpoolData.tokenMintA,
|
whirlpoolData.tokenMintA,
|
||||||
new u64(10000),
|
new u64(40_000_000),
|
||||||
slippageTolerance,
|
slippageTolerance,
|
||||||
ctx.program.programId,
|
ctx.program.programId,
|
||||||
fetcher,
|
fetcher,
|
||||||
|
@ -73,7 +123,57 @@ describe("swap arrays test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* |-------------c1-----|xxxxxxxxxxxxxxxxx|------c2-----------|
|
* |-------------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-----|xxc2xxxxxxxxxxxxx|-------------------|
|
||||||
*/
|
*/
|
||||||
it("3 sequential arrays, 2nd array not initialized, b->a", async () => {
|
it("3 sequential arrays, 2nd array not initialized, b->a", async () => {
|
||||||
const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing);
|
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 whirlpoolData = await whirlpool.refreshData();
|
||||||
const missingTickArray = PDAUtil.getTickArray(ctx.program.programId, whirlpool.getAddress(), 0);
|
const expectedError = "Swap input value traversed too many arrays.";
|
||||||
const expectedError = `[${missingTickArray.publicKey.toBase58()}] need to be initialized`;
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
swapQuoteByInputToken(
|
swapQuoteByInputToken(
|
||||||
whirlpool,
|
whirlpool,
|
||||||
whirlpoolData.tokenMintB,
|
whirlpoolData.tokenMintB,
|
||||||
new u64(10000),
|
new u64(40_000_000),
|
||||||
slippageTolerance,
|
slippageTolerance,
|
||||||
ctx.program.programId,
|
ctx.program.programId,
|
||||||
fetcher,
|
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|------------------|-----------------|-------------------|
|
* c1|------------------|-----------------|-------------------|
|
||||||
*/
|
*/
|
||||||
|
@ -536,4 +736,106 @@ describe("swap arrays test", () => {
|
||||||
assert.equal(quote.estimatedAmountIn.toString(), tradeAmount);
|
assert.equal(quote.estimatedAmountIn.toString(), tradeAmount);
|
||||||
assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute());
|
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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -428,53 +428,6 @@ describe("whirlpool-dev-fee-swap", () => {
|
||||||
(err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.InvalidDevFeePercentage
|
(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(
|
async function getQuotes(
|
||||||
|
|
|
@ -7,11 +7,15 @@ import {
|
||||||
buildWhirlpoolClient,
|
buildWhirlpoolClient,
|
||||||
MAX_TICK_INDEX,
|
MAX_TICK_INDEX,
|
||||||
MIN_TICK_INDEX,
|
MIN_TICK_INDEX,
|
||||||
|
MAX_SQRT_PRICE,
|
||||||
|
MIN_SQRT_PRICE,
|
||||||
PriceMath,
|
PriceMath,
|
||||||
swapQuoteByInputToken,
|
swapQuoteByInputToken,
|
||||||
swapQuoteByOutputToken,
|
swapQuoteByOutputToken,
|
||||||
|
swapQuoteWithParams,
|
||||||
TICK_ARRAY_SIZE,
|
TICK_ARRAY_SIZE,
|
||||||
WhirlpoolContext,
|
WhirlpoolContext,
|
||||||
|
SwapUtils,
|
||||||
} from "../../../../src";
|
} from "../../../../src";
|
||||||
import { SwapErrorCode, WhirlpoolsError } from "../../../../src/errors/errors";
|
import { SwapErrorCode, WhirlpoolsError } from "../../../../src/errors/errors";
|
||||||
import { assertInputOutputQuoteEqual, assertQuoteAndResults, TickSpacing } from "../../../utils";
|
import { assertInputOutputQuoteEqual, assertQuoteAndResults, TickSpacing } from "../../../utils";
|
||||||
|
@ -1165,4 +1169,108 @@ describe("swap traversal tests", () => {
|
||||||
const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData);
|
const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData);
|
||||||
assertQuoteAndResults(aToB, quote, newData, beforeVaultAmounts, afterVaultAmounts);
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,7 +23,10 @@ import { initTestPool } from "../../utils/init-utils";
|
||||||
import { mintTokensToTestAccount } from "../../utils/test-builders";
|
import { mintTokensToTestAccount } from "../../utils/test-builders";
|
||||||
|
|
||||||
describe("whirlpool-impl", () => {
|
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());
|
anchor.setProvider(anchor.AnchorProvider.env());
|
||||||
const program = anchor.workspace.Whirlpool;
|
const program = anchor.workspace.Whirlpool;
|
||||||
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
|
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
|
||||||
|
|
Loading…
Reference in New Issue