Skip to content

Commit

Permalink
Disallow non-full-range positions on pools with tick spacing >= 2^15 (#…
Browse files Browse the repository at this point in the history
…161)

* Disallow non-full-range positions on pools with tick spacing >= 2^15

* Tweaks

* Naming and small tweaks

* Update errors.rs
  • Loading branch information
wjthieme authored Jul 10, 2024
1 parent dd44649 commit 979bc97
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 13 deletions.
5 changes: 4 additions & 1 deletion programs/whirlpool/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ pub enum ErrorCode {

#[msg("Unable to call transfer hook without extra accounts")]
NoExtraAccountsForTransferHook, // 0x17a2 (6050)

#[msg("Output and input amount mismatch")]
IntermediateTokenAmountMismatch, // 0x17a3 (6051)

Expand All @@ -135,6 +135,9 @@ pub enum ErrorCode {

#[msg("Same accounts type is provided more than once")]
RemainingAccountsDuplicatedAccountsType, // 0x17a5 (6053)

#[msg("This whirlpool only supports full-range positions")]
FullRangeOnlyPool, // 0x17a6 (6054)
}

impl From<TryFromIntError> for ErrorCode {
Expand Down
2 changes: 2 additions & 0 deletions programs/whirlpool/src/math/tick_math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const BIT_PRECISION: u32 = 14;
const LOG_B_P_ERR_MARGIN_LOWER_X64: i128 = 184467440737095516i128; // 0.01
const LOG_B_P_ERR_MARGIN_UPPER_X64: i128 = 15793534762490258745i128; // 2^-precision / log_2_b + 0.01

pub const FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD: u16 = 32768; // 2^15

/// Derive the sqrt-price from a tick index. The precision of this method is only guarranted
/// if tick is within the bounds of {max, min} tick-index.
///
Expand Down
12 changes: 11 additions & 1 deletion programs/whirlpool/src/state/position.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anchor_lang::prelude::*;

use crate::{errors::ErrorCode, state::NUM_REWARDS};
use crate::{errors::ErrorCode, math::FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD, state::NUM_REWARDS};

use super::{Tick, Whirlpool};

Expand Down Expand Up @@ -69,6 +69,16 @@ impl Position {
return Err(ErrorCode::InvalidTickIndex.into());
}

// On tick spacing >= 2^15, should only be able to open full range positions
if whirlpool.tick_spacing >= FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD {
let (full_range_lower_index, full_range_upper_index) = Tick::full_range_indexes(whirlpool.tick_spacing);
if tick_lower_index != full_range_lower_index
|| tick_upper_index != full_range_upper_index
{
return Err(ErrorCode::FullRangeOnlyPool.into());
}
}

self.whirlpool = whirlpool.key();
self.position_mint = position_mint;

Expand Down
45 changes: 45 additions & 0 deletions programs/whirlpool/src/state/tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ impl Tick {
tick_index % tick_spacing as i32 == 0
}

pub fn full_range_indexes(tick_spacing: u16) -> (i32, i32) {
let lower_index = MIN_TICK_INDEX / tick_spacing as i32 * tick_spacing as i32;
let upper_index = MAX_TICK_INDEX / tick_spacing as i32 * tick_spacing as i32;
(lower_index, upper_index)
}

/// Bound a tick-index value to the max & min index value for this protocol
///
/// # Parameters
Expand Down Expand Up @@ -576,6 +582,45 @@ mod check_is_out_of_bounds_tests {
}
}

#[cfg(test)]
mod full_range_indexes_tests {
use crate::math::FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD;

use super::*;

#[test]
fn test_min_tick_spacing() {
assert_eq!(
Tick::full_range_indexes(1),
(MIN_TICK_INDEX, MAX_TICK_INDEX)
);
}

#[test]
fn test_standard_tick_spacing() {
assert_eq!(
Tick::full_range_indexes(128),
(-443520, 443520)
);
}

#[test]
fn test_full_range_only_tick_spacing() {
assert_eq!(
Tick::full_range_indexes(FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD),
(-425984, 425984)
);
}

#[test]
fn test_max_tick_spacing() {
assert_eq!(
Tick::full_range_indexes(u16::MAX),
(-393210, 393210)
);
}
}

#[cfg(test)]
mod array_update_tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions sdk/src/types/public/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,9 @@ export const FEE_RATE_MUL_VALUE = new BN(1_000_000);
export const WHIRLPOOL_NFT_UPDATE_AUTH = new PublicKey(
"3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"
);

/**
* The tick spacing (inclusive) at which a whirlpool only supports full-range positions.
* @category Constants
*/
export const FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD = 32768;
12 changes: 11 additions & 1 deletion sdk/src/utils/public/tick-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
WhirlpoolAccountFetcherInterface,
} from "../../network/public/fetcher";
import {
FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD,
MAX_TICK_INDEX,
MIN_TICK_INDEX,
TICK_ARRAY_SIZE,
Expand Down Expand Up @@ -173,7 +174,7 @@ export class TickUtil {

/**
* Get the minimum and maximum tick index that can be initialized.
*
*
* @param tickSpacing The tickSpacing for the Whirlpool
* @returns An array of numbers where the first element is the minimum tick index and the second element is the maximum tick index.
*/
Expand All @@ -195,6 +196,15 @@ export class TickUtil {
const [min, max] = TickUtil.getFullRangeTickIndex(tickSpacing);
return tickLowerIndex === min && tickUpperIndex === max;
}

/**
* Check if a whirlpool is a full-range only pool.
* @param tickSpacing The tickSpacing for the Whirlpool
* @returns true if the whirlpool is a full-range only pool, false otherwise.
*/
public static isFullRangeOnly(tickSpacing: number): boolean {
return tickSpacing >= FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD;
}
}

/**
Expand Down
54 changes: 50 additions & 4 deletions sdk/tests/integration/open_bundled_position.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
POSITION_BUNDLE_SIZE,
PositionBundleData,
PositionData,
TickUtil,
toTx,
WhirlpoolContext,
WhirlpoolIx
Expand All @@ -37,14 +38,20 @@ describe("open_bundled_position", () => {
const fetcher = ctx.fetcher;

const tickLowerIndex = 0;
const tickUpperIndex = 128;
const tickUpperIndex = 32768;
let poolInitInfo: InitPoolParams;
let whirlpoolPda: PDA;
let fullRangeOnlyPoolInitInfo: InitPoolParams;
let fullRangeOnlyWhirlpoolPda: PDA;
const funderKeypair = anchor.web3.Keypair.generate();

before(async () => {
poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo;
whirlpoolPda = poolInitInfo.whirlpoolPda;

fullRangeOnlyPoolInitInfo = (await initTestPool(ctx, TickSpacing.FullRangeOnly)).poolInitInfo;
fullRangeOnlyWhirlpoolPda = fullRangeOnlyPoolInitInfo.whirlpoolPda;

await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
});

Expand Down Expand Up @@ -85,9 +92,9 @@ describe("open_bundled_position", () => {
});
}

function checkPositionAccountContents(position: PositionData, mint: PublicKey) {
assert.strictEqual(position.tickLowerIndex, tickLowerIndex);
assert.strictEqual(position.tickUpperIndex, tickUpperIndex);
function checkPositionAccountContents(position: PositionData, mint: PublicKey, lowerTick: number = tickLowerIndex, upperTick: number = tickUpperIndex) {
assert.strictEqual(position.tickLowerIndex, lowerTick);
assert.strictEqual(position.tickUpperIndex, upperTick);
assert.ok(position.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey));
assert.ok(position.positionMint.equals(mint));
assert.ok(position.liquidity.eq(ZERO_BN));
Expand Down Expand Up @@ -204,6 +211,29 @@ describe("open_bundled_position", () => {
checkBitmap(positionBundle, bundleIndexes);
});

it("successfully opens bundled position for full-range only pool", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);

const [lowerTickIndex, upperTickIndex] = TickUtil.getFullRangeTickIndex(TickSpacing.FullRangeOnly);

const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
fullRangeOnlyWhirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
lowerTickIndex,
upperTickIndex
);
const { bundledPositionPda } = positionInitInfo.params;

const position = (await fetcher.getPosition(bundledPositionPda.publicKey)) as PositionData;
checkPositionAccountContents(position, positionBundleInfo.positionBundleMintKeypair.publicKey, lowerTickIndex, upperTickIndex);

const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, IGNORE_CACHE)) as PositionBundleData;
checkBitmap(positionBundle, [bundleIndex]);
});

describe("invalid bundle index", () => {
it("should be failed: invalid bundle index (< 0)", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
Expand Down Expand Up @@ -603,4 +633,20 @@ describe("open_bundled_position", () => {
});
});

it("fail when opening a non-full range position in an full-range only pool", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
await assert.rejects(
openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
),
/0x17a6/ // FullRangeOnlyPool
);
});

});
50 changes: 47 additions & 3 deletions sdk/tests/integration/open_position.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
OpenPositionParams,
PDAUtil,
PositionData,
TickUtil,
WhirlpoolContext,
WhirlpoolIx,
toTx
Expand Down Expand Up @@ -38,20 +39,25 @@ describe("open_position", () => {
let defaultParams: OpenPositionParams;
let defaultMint: Keypair;
const tickLowerIndex = 0;
const tickUpperIndex = 128;
const tickUpperIndex = 32768;
let poolInitInfo: InitPoolParams;
let whirlpoolPda: PDA;
let fullRangeOnlyPoolInitInfo: InitPoolParams;
let fullRangeOnlyWhirlpoolPda: PDA;
const funderKeypair = anchor.web3.Keypair.generate();

before(async () => {
poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo;
whirlpoolPda = poolInitInfo.whirlpoolPda;

fullRangeOnlyPoolInitInfo = (await initTestPool(ctx, TickSpacing.FullRangeOnly)).poolInitInfo;
fullRangeOnlyWhirlpoolPda = fullRangeOnlyPoolInitInfo.whirlpoolPda;

const { params, mint } = await generateDefaultOpenPositionParams(
ctx,
whirlpoolPda.publicKey,
0,
128,
tickLowerIndex,
tickUpperIndex,
provider.wallet.publicKey
);
defaultParams = params;
Expand Down Expand Up @@ -83,6 +89,30 @@ describe("open_position", () => {
// TODO: Add tests for rewards
});

it("successfully open position and verify position address contents for full-range only pool", async () => {
const [lowerTickIndex, upperTickIndex] = TickUtil.getFullRangeTickIndex(TickSpacing.FullRangeOnly);

const positionInitInfo = await openPosition(
ctx,
fullRangeOnlyWhirlpoolPda.publicKey,
lowerTickIndex,
upperTickIndex
);
const { positionPda, positionMintAddress } = positionInitInfo.params;

const position = (await fetcher.getPosition(positionPda.publicKey)) as PositionData;

assert.strictEqual(position.tickLowerIndex, lowerTickIndex);
assert.strictEqual(position.tickUpperIndex, upperTickIndex);
assert.ok(position.whirlpool.equals(fullRangeOnlyPoolInitInfo.whirlpoolPda.publicKey));
assert.ok(position.positionMint.equals(positionMintAddress));
assert.ok(position.liquidity.eq(ZERO_BN));
assert.ok(position.feeGrowthCheckpointA.eq(ZERO_BN));
assert.ok(position.feeGrowthCheckpointB.eq(ZERO_BN));
assert.ok(position.feeOwedA.eq(ZERO_BN));
assert.ok(position.feeOwedB.eq(ZERO_BN));
});

it("succeeds when funder is different than account paying for transaction fee", async () => {
await openPosition(
ctx,
Expand Down Expand Up @@ -211,4 +241,18 @@ describe("open_position", () => {
/0x0/
);
});

it("fail when opening a non-full range position in an full-range only pool", async () => {
await assert.rejects(
openPosition(
ctx,
fullRangeOnlyWhirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex,
provider.wallet.publicKey,
funderKeypair
),
/0x17a6/ // FullRangeOnlyPool
);
});
});
Loading

0 comments on commit 979bc97

Please sign in to comment.