Open Source commit for Orca Whirlpools (#1)

This commit is contained in:
meep 2022-05-02 20:32:00 -07:00 committed by GitHub
parent f0b4de883f
commit c55588a6ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
167 changed files with 129474 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.anchor
.DS_Store
.vscode/
target
**/*.rs.bk
node_modules
test-ledger/
sdk/dist/
sdk/node_modules

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"files.insertFinalNewline": true
}

17
Anchor.toml Normal file
View File

@ -0,0 +1,17 @@
[programs.localnet]
whirlpool = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"
[registry]
url = "https://anchor.projectserum.com"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
# wait for update to anchor to support cloning
# [[test.genesis]]
# address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
# program = "../metaplex-program-library/token-metadata/target/deploy/mpl_token_metadata.so"
[scripts]
test = "ts-mocha -p sdk/tests/tsconfig.json -t 1000000 sdk/tests/**/*.ts"

1676
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

4
Cargo.toml Normal file
View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

32
README.md Normal file
View File

@ -0,0 +1,32 @@
# Whirlpool
## Required Setup
- Go through [Anchor install guide](https://project-serum.github.io/anchor/getting-started/installation.html#install-rust)
- Need to have a valid Solana keypair at `~/.config/solana/id.json` to do local testing with `anchor test` flows.
## Required npm globally installed packages
- mocha (I think? Could try just ts-mocha first)
- ts-mocha
- typescript
Also your $NODE_PATH must be set to the `node_modules` directory of your global installs.
For me since I am using Node 16.10.0 installed through `nvm`, my $NODE_PATH is the following:
```
$ echo $NODE_PATH
/Users/<home_dir>/.nvm/versions/node/v16.10.0/lib/node_modules
```
## Minimum Requirements
- Node 16.4 (Anchor)
- Anchor 0.20.1
- Solana 1.9.3
- Rust 1.59.0
## Unit Tests
- Run "cargo test --lib" to run unit tests
- Run "anchor test" to run integration tests

12
migrations/deploy.js Normal file
View File

@ -0,0 +1,12 @@
// Migrations are an early feature. Currently, they're nothing more than this
// single deploy script that's invoked from the CLI, injecting a provider
// configured from the workspace's Anchor.toml.
const anchor = require("@project-serum/anchor");
module.exports = async function (provider) {
// Configure client to use the provider.
anchor.setProvider(provider);
// Add your deploy script here.
}

3179
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

11
package.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "orca-whirlpools",
"private": true,
"workspaces": [
"sdk",
"scripts"
],
"scripts": {
"idl": "anchor build -i $INIT_CWD/sdk/src/artifacts -t $INIT_CWD/sdk/src/artifacts"
}
}

View File

@ -0,0 +1,34 @@
[package]
name = "whirlpool"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "whirlpool"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = "0.20.1"
anchor-spl = "0.20.1"
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
solana-program = "1.8.12"
thiserror = "1.0"
uint = { version = "0.9.1", default-features = false }
borsh = "0.9.1"
mpl-token-metadata = { version = "1.2.5", features = ["no-entrypoint"] }
[dev-dependencies]
proptest = "1.0"
serde = "1.0.117"
serde_json = "1.0.59"
[dev-dependencies.serde_with]
version = "1.12.0"
features = ["json"]

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,3 @@
pub mod test_constants;
pub use test_constants::*;

View File

@ -0,0 +1,9 @@
#[cfg(test)]
use anchor_lang::prelude::Pubkey;
#[cfg(test)]
pub fn test_program_id() -> Pubkey {
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"
.parse()
.unwrap()
}

View File

@ -0,0 +1,110 @@
use std::num::TryFromIntError;
use anchor_lang::error;
#[error]
#[derive(PartialEq)]
pub enum ErrorCode {
#[msg("Enum value could not be converted")]
InvalidEnum, // 0x1770
#[msg("Invalid start tick index provided.")]
InvalidStartTick, // 0x1771
#[msg("Tick-array already exists in this whirlpool")]
TickArrayExistInPool, // 0x1772
#[msg("Attempt to search for a tick-array failed")]
TickArrayIndexOutofBounds, // 0x1773
#[msg("Tick-spacing is not supported")]
InvalidTickSpacing, // 0x1774
#[msg("Position is not empty It cannot be closed")]
ClosePositionNotEmpty, // 0x1775
#[msg("Unable to divide by zero")]
DivideByZero, // 0x1776
#[msg("Unable to cast number into BigInt")]
NumberCastError, // 0x1777
#[msg("Unable to down cast number")]
NumberDownCastError, // 0x1778
#[msg("Tick not found within tick array")]
TickNotFound, // 0x1779
#[msg("Provided tick index is either out of bounds or uninitializable")]
InvalidTickIndex, // 0x177a
#[msg("Provided sqrt price out of bounds")]
SqrtPriceOutOfBounds, // 0x177b
#[msg("Liquidity amount must be greater than zero")]
LiquidityZero, // 0x177c
#[msg("Liquidity amount must be less than i64::MAX")]
LiquidityTooHigh, // 0x177d
#[msg("Liquidity overflow")]
LiquidityOverflow, // 0x177e
#[msg("Liquidity underflow")]
LiquidityUnderflow, // 0x177f
#[msg("Tick liquidity net underflowed or overflowed")]
LiquidityNetError, // 0x1780
#[msg("Exceeded token max")]
TokenMaxExceeded, // 0x1781
#[msg("Did not meet token min")]
TokenMinSubceeded, // 0x1782
#[msg("Position token account has a missing or invalid delegate")]
MissingOrInvalidDelegate, // 0x1783
#[msg("Position token amount must be 1")]
InvalidPositionTokenAmount, // 0x1784
#[msg("Timestamp should be convertible from i64 to u64")]
InvalidTimestampConversion, // 0x1785
#[msg("Timestamp should be greater than the last updated timestamp")]
InvalidTimestamp, // 0x1786
#[msg("Invalid tick array sequence provided for instruction.")]
InvalidTickArraySequence, // 0x1787
#[msg("Token Mint in wrong order")]
InvalidTokenMintOrder, // 0x1788
#[msg("Reward not initialized")]
RewardNotInitialized, // 0x1789
#[msg("Invalid reward index")]
InvalidRewardIndex, // 0x178a
#[msg("Reward vault requires amount to support emissions for at least one day")]
RewardVaultAmountInsufficient, // 0x178b
#[msg("Exceeded max fee rate")]
FeeRateMaxExceeded, // 0x178c
#[msg("Exceeded max protocol fee rate")]
ProtocolFeeRateMaxExceeded, // 0x178d
#[msg("Multiplication with shift right overflow")]
MultiplicationShiftRightOverflow, // 0x178e
#[msg("Muldiv overflow")]
MulDivOverflow, // 0x178f
#[msg("Invalid div_u256 input")]
MulDivInvalidInput, //0x1790
#[msg("Multiplication overflow")]
MultiplicationOverflow, //0x1791
#[msg("Provided SqrtPriceLimit not in the same direction as the swap.")]
InvalidSqrtPriceLimitDirection, //0x1792
#[msg("There are no tradable amount to swap.")]
ZeroTradableAmount, //0x1793
#[msg("Amount out below minimum threshold")]
AmountOutBelowMinimum, //0x1794
#[msg("Amount in above maximum threshold")]
AmountInAboveMaximum, //0x1795
#[msg("Invalid index for tick array sequence")]
TickArraySequenceInvalidIndex, //0x1796
#[msg("Amount calculated overflows")]
AmountCalcOverflow, //0x1797
#[msg("Amount remaining overflows")]
AmountRemainingOverflow, //0x1798
}
impl From<TryFromIntError> for ErrorCode {
fn from(_: TryFromIntError) -> Self {
ErrorCode::NumberCastError
}
}

View File

@ -0,0 +1,47 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
use crate::errors::ErrorCode;
use crate::state::*;
use crate::util::{burn_and_close_user_position_token, verify_position_authority};
#[derive(Accounts)]
pub struct ClosePosition<'info> {
pub position_authority: Signer<'info>,
#[account(mut)]
pub receiver: UncheckedAccount<'info>,
#[account(mut, close = receiver)]
pub position: Account<'info, Position>,
#[account(mut, address = position.position_mint)]
pub position_mint: Account<'info, Mint>,
#[account(mut,
constraint = position_token_account.amount == 1,
constraint = position_token_account.mint == position.position_mint)]
pub position_token_account: Box<Account<'info, TokenAccount>>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: Context<ClosePosition>) -> ProgramResult {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,
)?;
if !Position::is_position_empty(&ctx.accounts.position) {
return Err(ErrorCode::ClosePositionNotEmpty.into());
}
burn_and_close_user_position_token(
&ctx.accounts.position_authority,
&ctx.accounts.receiver,
&ctx.accounts.position_mint,
&ctx.accounts.position_token_account,
&ctx.accounts.token_program,
)
}

View File

@ -0,0 +1,68 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};
use crate::{
state::*,
util::{transfer_from_vault_to_owner, verify_position_authority},
};
#[derive(Accounts)]
pub struct CollectFees<'info> {
pub whirlpool: Box<Account<'info, Whirlpool>>,
pub position_authority: Signer<'info>,
#[account(mut, has_one = whirlpool)]
pub position: Box<Account<'info, Position>>,
#[account(
constraint = position_token_account.mint == position.position_mint,
constraint = position_token_account.amount == 1
)]
pub position_token_account: Box<Account<'info, TokenAccount>>,
#[account(mut, constraint = token_owner_account_a.mint == whirlpool.token_mint_a)]
pub token_owner_account_a: Box<Account<'info, TokenAccount>>,
#[account(mut, address = whirlpool.token_vault_a)]
pub token_vault_a: Box<Account<'info, TokenAccount>>,
#[account(mut, constraint = token_owner_account_b.mint == whirlpool.token_mint_b)]
pub token_owner_account_b: Box<Account<'info, TokenAccount>>,
#[account(mut, address = whirlpool.token_vault_b)]
pub token_vault_b: Box<Account<'info, TokenAccount>>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: Context<CollectFees>) -> ProgramResult {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,
)?;
let position = &mut ctx.accounts.position;
// Store the fees owed to use as transfer amounts.
let fee_owed_a = position.fee_owed_a;
let fee_owed_b = position.fee_owed_b;
position.reset_fees_owed();
transfer_from_vault_to_owner(
&ctx.accounts.whirlpool,
&ctx.accounts.token_vault_a,
&ctx.accounts.token_owner_account_a,
&ctx.accounts.token_program,
fee_owed_a,
)?;
transfer_from_vault_to_owner(
&ctx.accounts.whirlpool,
&ctx.accounts.token_vault_b,
&ctx.accounts.token_owner_account_b,
&ctx.accounts.token_program,
fee_owed_b,
)?;
Ok(())
}

View File

@ -0,0 +1,51 @@
use crate::{state::*, util::transfer_from_vault_to_owner};
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};
#[derive(Accounts)]
pub struct CollectProtocolFees<'info> {
pub whirlpools_config: Box<Account<'info, WhirlpoolsConfig>>,
#[account(mut, has_one = whirlpools_config)]
pub whirlpool: Box<Account<'info, Whirlpool>>,
#[account(address = whirlpools_config.collect_protocol_fees_authority)]
pub collect_protocol_fees_authority: Signer<'info>,
#[account(mut, address = whirlpool.token_vault_a)]
pub token_vault_a: Account<'info, TokenAccount>,
#[account(mut, address = whirlpool.token_vault_b)]
pub token_vault_b: Account<'info, TokenAccount>,
#[account(mut, constraint = token_destination_a.mint == whirlpool.token_mint_a)]
pub token_destination_a: Account<'info, TokenAccount>,
#[account(mut, constraint = token_destination_b.mint == whirlpool.token_mint_b)]
pub token_destination_b: Account<'info, TokenAccount>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: Context<CollectProtocolFees>) -> ProgramResult {
let whirlpool = &ctx.accounts.whirlpool;
transfer_from_vault_to_owner(
whirlpool,
&ctx.accounts.token_vault_a,
&ctx.accounts.token_destination_a,
&ctx.accounts.token_program,
whirlpool.protocol_fee_owed_a,
)?;
transfer_from_vault_to_owner(
whirlpool,
&ctx.accounts.token_vault_b,
&ctx.accounts.token_destination_b,
&ctx.accounts.token_program,
whirlpool.protocol_fee_owed_b,
)?;
Ok(ctx.accounts.whirlpool.reset_protocol_fees_owed())
}

View File

@ -0,0 +1,114 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};
use crate::{
state::*,
util::{transfer_from_vault_to_owner, verify_position_authority},
};
#[derive(Accounts)]
#[instruction(reward_index: u8)]
pub struct CollectReward<'info> {
pub whirlpool: Box<Account<'info, Whirlpool>>,
pub position_authority: Signer<'info>,
#[account(mut, has_one = whirlpool)]
pub position: Box<Account<'info, Position>>,
#[account(
constraint = position_token_account.mint == position.position_mint,
constraint = position_token_account.amount == 1
)]
pub position_token_account: Box<Account<'info, TokenAccount>>,
#[account(mut,
constraint = reward_owner_account.mint == whirlpool.reward_infos[reward_index as usize].mint
)]
pub reward_owner_account: Box<Account<'info, TokenAccount>>,
#[account(mut, address = whirlpool.reward_infos[reward_index as usize].vault)]
pub reward_vault: Box<Account<'info, TokenAccount>>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
}
/// Collects all harvestable tokens for a specified reward.
///
/// If the Whirlpool reward vault does not have enough tokens, the maximum number of available
/// tokens will be debited to the user. The unharvested amount remains tracked, and it can be
/// harvested in the future.
///
/// # Parameters
/// - `reward_index` - The reward to harvest. Acceptable values are 0, 1, and 2.
///
/// # Returns
/// - `Ok`: Reward tokens at the specified reward index have been successfully harvested
/// - `Err`: `RewardNotInitialized` if the specified reward has not been initialized
/// `InvalidRewardIndex` if the reward index is not 0, 1, or 2
pub fn handler(ctx: Context<CollectReward>, reward_index: u8) -> ProgramResult {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,
)?;
let index = reward_index as usize;
let position = &mut ctx.accounts.position;
let (transfer_amount, updated_amount_owed) = calculate_collect_reward(
position.reward_infos[index],
ctx.accounts.reward_vault.amount,
);
position.update_reward_owed(index, updated_amount_owed);
Ok(transfer_from_vault_to_owner(
&ctx.accounts.whirlpool,
&ctx.accounts.reward_vault,
&ctx.accounts.reward_owner_account,
&ctx.accounts.token_program,
transfer_amount,
)?)
}
fn calculate_collect_reward(position_reward: PositionRewardInfo, vault_amount: u64) -> (u64, u64) {
let amount_owed = position_reward.amount_owed;
let (transfer_amount, updated_amount_owed) = if amount_owed > vault_amount {
(vault_amount, amount_owed - vault_amount)
} else {
(amount_owed, 0)
};
(transfer_amount, updated_amount_owed)
}
#[cfg(test)]
mod unit_tests {
use super::calculate_collect_reward;
use crate::state::PositionRewardInfo;
#[test]
fn test_calculate_collect_reward_vault_insufficient_tokens() {
let (transfer_amount, updated_amount_owed) =
calculate_collect_reward(position_reward(10), 1);
assert_eq!(transfer_amount, 1);
assert_eq!(updated_amount_owed, 9);
}
#[test]
fn test_calculate_collect_reward_vault_sufficient_tokens() {
let (transfer_amount, updated_amount_owed) =
calculate_collect_reward(position_reward(10), 10);
assert_eq!(transfer_amount, 10);
assert_eq!(updated_amount_owed, 0);
}
fn position_reward(amount_owed: u64) -> PositionRewardInfo {
PositionRewardInfo {
amount_owed,
..Default::default()
}
}
}

View File

@ -0,0 +1,82 @@
use anchor_lang::prelude::*;
use crate::errors::ErrorCode;
use crate::manager::liquidity_manager::{
calculate_liquidity_token_deltas, calculate_modify_liquidity, sync_modify_liquidity_values,
};
use crate::math::convert_to_liquidity_delta;
use crate::util::{to_timestamp_u64, transfer_from_vault_to_owner, verify_position_authority};
use super::ModifyLiquidity;
/*
Removes liquidity from an existing Whirlpool Position.
*/
pub fn handler(
ctx: Context<ModifyLiquidity>,
liquidity_amount: u128,
token_min_a: u64,
token_min_b: u64,
) -> ProgramResult {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,
)?;
let clock = Clock::get()?;
if liquidity_amount == 0 {
return Err(ErrorCode::LiquidityZero.into());
}
let liquidity_delta = convert_to_liquidity_delta(liquidity_amount, false)?;
let timestamp = to_timestamp_u64(clock.unix_timestamp)?;
let update = calculate_modify_liquidity(
&ctx.accounts.whirlpool,
&ctx.accounts.position,
&ctx.accounts.tick_array_lower,
&ctx.accounts.tick_array_upper,
liquidity_delta,
timestamp,
)?;
sync_modify_liquidity_values(
&mut ctx.accounts.whirlpool,
&mut ctx.accounts.position,
&ctx.accounts.tick_array_lower,
&ctx.accounts.tick_array_upper,
update,
timestamp,
)?;
let (delta_a, delta_b) = calculate_liquidity_token_deltas(
ctx.accounts.whirlpool.tick_current_index,
ctx.accounts.whirlpool.sqrt_price,
&ctx.accounts.position,
liquidity_delta,
)?;
if delta_a < token_min_a {
return Err(ErrorCode::TokenMinSubceeded.into());
} else if delta_b < token_min_b {
return Err(ErrorCode::TokenMinSubceeded.into());
}
transfer_from_vault_to_owner(
&ctx.accounts.whirlpool,
&ctx.accounts.token_vault_a,
&ctx.accounts.token_owner_account_a,
&ctx.accounts.token_program,
delta_a,
)?;
transfer_from_vault_to_owner(
&ctx.accounts.whirlpool,
&ctx.accounts.token_vault_b,
&ctx.accounts.token_owner_account_b,
&ctx.accounts.token_program,
delta_b,
)?;
Ok(())
}

View File

@ -0,0 +1,113 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};
use crate::errors::ErrorCode;
use crate::manager::liquidity_manager::{
calculate_liquidity_token_deltas, calculate_modify_liquidity, sync_modify_liquidity_values,
};
use crate::math::convert_to_liquidity_delta;
use crate::state::*;
use crate::util::{to_timestamp_u64, transfer_from_owner_to_vault, verify_position_authority};
#[derive(Accounts)]
pub struct ModifyLiquidity<'info> {
#[account(mut)]
pub whirlpool: Account<'info, Whirlpool>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
pub position_authority: Signer<'info>,
#[account(mut, has_one = whirlpool)]
pub position: Account<'info, Position>,
#[account(
constraint = position_token_account.mint == position.position_mint,
constraint = position_token_account.amount == 1
)]
pub position_token_account: Box<Account<'info, TokenAccount>>,
#[account(mut, constraint = token_owner_account_a.mint == whirlpool.token_mint_a)]
pub token_owner_account_a: Box<Account<'info, TokenAccount>>,
#[account(mut, constraint = token_owner_account_b.mint == whirlpool.token_mint_b)]
pub token_owner_account_b: Box<Account<'info, TokenAccount>>,
#[account(mut, constraint = token_vault_a.key() == whirlpool.token_vault_a)]
pub token_vault_a: Box<Account<'info, TokenAccount>>,
#[account(mut, constraint = token_vault_b.key() == whirlpool.token_vault_b)]
pub token_vault_b: Box<Account<'info, TokenAccount>>,
#[account(mut, has_one = whirlpool)]
pub tick_array_lower: AccountLoader<'info, TickArray>,
#[account(mut, has_one = whirlpool)]
pub tick_array_upper: AccountLoader<'info, TickArray>,
}
pub fn handler(
ctx: Context<ModifyLiquidity>,
liquidity_amount: u128,
token_max_a: u64,
token_max_b: u64,
) -> ProgramResult {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,
)?;
let clock = Clock::get()?;
if liquidity_amount == 0 {
return Err(ErrorCode::LiquidityZero.into());
}
let liquidity_delta = convert_to_liquidity_delta(liquidity_amount, true)?;
let timestamp = to_timestamp_u64(clock.unix_timestamp)?;
let update = calculate_modify_liquidity(
&ctx.accounts.whirlpool,
&ctx.accounts.position,
&ctx.accounts.tick_array_lower,
&ctx.accounts.tick_array_upper,
liquidity_delta,
timestamp,
)?;
sync_modify_liquidity_values(
&mut ctx.accounts.whirlpool,
&mut ctx.accounts.position,
&ctx.accounts.tick_array_lower,
&ctx.accounts.tick_array_upper,
update,
timestamp,
)?;
let (delta_a, delta_b) = calculate_liquidity_token_deltas(
ctx.accounts.whirlpool.tick_current_index,
ctx.accounts.whirlpool.sqrt_price,
&ctx.accounts.position,
liquidity_delta,
)?;
if delta_a > token_max_a {
return Err(ErrorCode::TokenMaxExceeded.into());
} else if delta_b > token_max_b {
return Err(ErrorCode::TokenMaxExceeded.into());
}
transfer_from_owner_to_vault(
&ctx.accounts.position_authority,
&ctx.accounts.token_owner_account_a,
&ctx.accounts.token_vault_a,
&ctx.accounts.token_program,
delta_a,
)?;
transfer_from_owner_to_vault(
&ctx.accounts.position_authority,
&ctx.accounts.token_owner_account_b,
&ctx.accounts.token_vault_b,
&ctx.accounts.token_program,
delta_b,
)?;
Ok(())
}

View File

@ -0,0 +1,31 @@
use anchor_lang::prelude::*;
use crate::state::*;
#[derive(Accounts)]
pub struct InitializeConfig<'info> {
#[account(init, payer = funder, space = WhirlpoolsConfig::LEN)]
pub config: Account<'info, WhirlpoolsConfig>,
#[account(mut)]
pub funder: Signer<'info>,
pub system_program: Program<'info, System>,
}
pub fn handler(
ctx: Context<InitializeConfig>,
fee_authority: Pubkey,
collect_protocol_fees_authority: Pubkey,
reward_emissions_super_authority: Pubkey,
default_protocol_fee_rate: u16,
) -> ProgramResult {
let config = &mut ctx.accounts.config;
Ok(config.initialize(
fee_authority,
collect_protocol_fees_authority,
reward_emissions_super_authority,
default_protocol_fee_rate,
)?)
}

View File

@ -0,0 +1,35 @@
use crate::state::*;
use anchor_lang::prelude::*;
#[derive(Accounts)]
#[instruction(tick_spacing: u16)]
pub struct InitializeFeeTier<'info> {
pub config: Box<Account<'info, WhirlpoolsConfig>>,
#[account(init,
payer = funder,
seeds = [b"fee_tier", config.key().as_ref(),
tick_spacing.to_le_bytes().as_ref()],
bump,
space = FeeTier::LEN)]
pub fee_tier: Account<'info, FeeTier>,
#[account(mut)]
pub funder: Signer<'info>,
#[account(address = config.fee_authority)]
pub fee_authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
pub fn handler(
ctx: Context<InitializeFeeTier>,
tick_spacing: u16,
default_fee_rate: u16,
) -> ProgramResult {
Ok(ctx
.accounts
.fee_tier
.initialize(&ctx.accounts.config, tick_spacing, default_fee_rate)?)
}

View File

@ -0,0 +1,75 @@
use crate::state::*;
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
#[derive(Accounts)]
#[instruction(bumps: WhirlpoolBumps, tick_spacing: u16)]
pub struct InitializePool<'info> {
pub whirlpools_config: Box<Account<'info, WhirlpoolsConfig>>,
pub token_mint_a: Account<'info, Mint>,
pub token_mint_b: Account<'info, Mint>,
#[account(mut)]
pub funder: Signer<'info>,
#[account(init,
seeds = [
b"whirlpool".as_ref(),
whirlpools_config.key().as_ref(),
token_mint_a.key().as_ref(),
token_mint_b.key().as_ref(),
tick_spacing.to_le_bytes().as_ref()
],
bump = bumps.whirlpool_bump,
payer = funder,
space = Whirlpool::LEN)]
pub whirlpool: Box<Account<'info, Whirlpool>>,
#[account(init,
payer = funder,
token::mint = token_mint_a,
token::authority = whirlpool)]
pub token_vault_a: Box<Account<'info, TokenAccount>>,
#[account(init,
payer = funder,
token::mint = token_mint_b,
token::authority = whirlpool)]
pub token_vault_b: Box<Account<'info, TokenAccount>>,
#[account(has_one = whirlpools_config)]
pub fee_tier: Account<'info, FeeTier>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
pub fn handler(
ctx: Context<InitializePool>,
bumps: WhirlpoolBumps,
tick_spacing: u16,
initial_sqrt_price: u128,
) -> ProgramResult {
let token_mint_a = ctx.accounts.token_mint_a.key();
let token_mint_b = ctx.accounts.token_mint_b.key();
let whirlpool = &mut ctx.accounts.whirlpool;
let whirlpools_config = &ctx.accounts.whirlpools_config;
let default_fee_rate = ctx.accounts.fee_tier.default_fee_rate;
Ok(whirlpool.initialize(
whirlpools_config,
bumps.whirlpool_bump,
tick_spacing,
initial_sqrt_price,
default_fee_rate,
token_mint_a,
ctx.accounts.token_vault_a.key(),
token_mint_b,
ctx.accounts.token_vault_b.key(),
)?)
}

View File

@ -0,0 +1,42 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
use crate::state::Whirlpool;
#[derive(Accounts)]
#[instruction(reward_index: u8)]
pub struct InitializeReward<'info> {
#[account(address = whirlpool.reward_infos[reward_index as usize].authority)]
pub reward_authority: Signer<'info>,
#[account(mut)]
pub funder: Signer<'info>,
#[account(mut)]
pub whirlpool: Box<Account<'info, Whirlpool>>,
pub reward_mint: Box<Account<'info, Mint>>,
#[account(
init,
payer = funder,
token::mint = reward_mint,
token::authority = whirlpool
)]
pub reward_vault: Box<Account<'info, TokenAccount>>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
pub fn handler(ctx: Context<InitializeReward>, reward_index: u8) -> ProgramResult {
let whirlpool = &mut ctx.accounts.whirlpool;
Ok(whirlpool.initialize_reward(
reward_index as usize,
ctx.accounts.reward_mint.key(),
ctx.accounts.reward_vault.key(),
)?)
}

View File

@ -0,0 +1,27 @@
use anchor_lang::prelude::*;
use crate::state::*;
#[derive(Accounts)]
#[instruction(start_tick_index: i32)]
pub struct InitializeTickArray<'info> {
pub whirlpool: Account<'info, Whirlpool>,
#[account(mut)]
pub funder: Signer<'info>,
#[account(
init,
payer = funder,
seeds = [b"tick_array", whirlpool.key().as_ref(), start_tick_index.to_string().as_bytes()],
bump,
space = TickArray::LEN)]
pub tick_array: AccountLoader<'info, TickArray>,
pub system_program: Program<'info, System>,
}
pub fn handler(ctx: Context<InitializeTickArray>, start_tick_index: i32) -> ProgramResult {
let mut tick_array = ctx.accounts.tick_array.load_init()?;
Ok(tick_array.initialize(&ctx.accounts.whirlpool, start_tick_index)?)
}

View File

@ -0,0 +1,51 @@
pub mod close_position;
pub mod collect_fees;
pub mod collect_protocol_fees;
pub mod collect_reward;
pub mod decrease_liquidity;
pub mod increase_liquidity;
pub mod initialize_config;
pub mod initialize_fee_tier;
pub mod initialize_pool;
pub mod initialize_reward;
pub mod initialize_tick_array;
pub mod open_position;
pub mod open_position_with_metadata;
pub mod set_collect_protocol_fees_authority;
pub mod set_default_fee_rate;
pub mod set_default_protocol_fee_rate;
pub mod set_fee_authority;
pub mod set_fee_rate;
pub mod set_protocol_fee_rate;
pub mod set_reward_authority;
pub mod set_reward_authority_by_super_authority;
pub mod set_reward_emissions;
pub mod set_reward_emissions_super_authority;
pub mod swap;
pub mod update_fees_and_rewards;
pub use close_position::*;
pub use collect_fees::*;
pub use collect_protocol_fees::*;
pub use collect_reward::*;
pub use decrease_liquidity::*;
pub use increase_liquidity::*;
pub use initialize_config::*;
pub use initialize_fee_tier::*;
pub use initialize_pool::*;
pub use initialize_reward::*;
pub use initialize_tick_array::*;
pub use open_position::*;
pub use open_position_with_metadata::*;
pub use set_collect_protocol_fees_authority::*;
pub use set_default_fee_rate::*;
pub use set_default_protocol_fee_rate::*;
pub use set_fee_authority::*;
pub use set_fee_rate::*;
pub use set_protocol_fee_rate::*;
pub use set_reward_authority::*;
pub use set_reward_authority_by_super_authority::*;
pub use set_reward_emissions::*;
pub use set_reward_emissions_super_authority::*;
pub use swap::*;
pub use update_fees_and_rewards::*;

View File

@ -0,0 +1,73 @@
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
use crate::{state::*, util::mint_position_token_and_remove_authority};
#[derive(Accounts)]
#[instruction(bumps: OpenPositionBumps)]
pub struct OpenPosition<'info> {
#[account(mut)]
pub funder: Signer<'info>,
pub owner: UncheckedAccount<'info>,
#[account(init,
payer = funder,
space = Position::LEN,
seeds = [b"position".as_ref(), position_mint.key().as_ref()],
bump = bumps.position_bump,
)]
pub position: Box<Account<'info, Position>>,
#[account(init,
payer = funder,
space = Mint::LEN,
mint::authority = whirlpool,
mint::decimals = 0,
)]
pub position_mint: Account<'info, Mint>,
#[account(init,
payer = funder,
associated_token::mint = position_mint,
associated_token::authority = owner,
)]
pub position_token_account: Box<Account<'info, TokenAccount>>,
pub whirlpool: Box<Account<'info, Whirlpool>>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
pub associated_token_program: Program<'info, AssociatedToken>,
}
/*
Opens a new Whirlpool Position.
*/
pub fn handler(
ctx: Context<OpenPosition>,
_bumps: OpenPositionBumps,
tick_lower_index: i32,
tick_upper_index: i32,
) -> ProgramResult {
let whirlpool = &ctx.accounts.whirlpool;
let position_mint = &ctx.accounts.position_mint;
let position = &mut ctx.accounts.position;
position.open_position(
whirlpool,
position_mint.key(),
tick_lower_index,
tick_upper_index,
)?;
mint_position_token_and_remove_authority(
whirlpool,
position_mint,
&ctx.accounts.position_token_account,
&ctx.accounts.token_program,
)
}

View File

@ -0,0 +1,98 @@
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
use crate::{state::*, util::mint_position_token_with_metadata_and_remove_authority};
use whirlpool_nft_update_auth::ID as WP_NFT_UPDATE_AUTH;
mod whirlpool_nft_update_auth {
use super::*;
declare_id!("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr");
}
#[derive(Accounts)]
#[instruction(bumps: OpenPositionWithMetadataBumps)]
pub struct OpenPositionWithMetadata<'info> {
#[account(mut)]
pub funder: Signer<'info>,
pub owner: UncheckedAccount<'info>,
#[account(init,
payer = funder,
space = Position::LEN,
seeds = [b"position".as_ref(), position_mint.key().as_ref()],
bump = bumps.position_bump,
)]
pub position: Box<Account<'info, Position>>,
#[account(init,
payer = funder,
space = Mint::LEN,
mint::authority = whirlpool,
mint::decimals = 0,
)]
pub position_mint: Account<'info, Mint>,
/// CHECK: checked via the Metadata CPI call
/// https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/utils.rs#L873
#[account(mut)]
pub position_metadata_account: UncheckedAccount<'info>,
#[account(init,
payer = funder,
associated_token::mint = position_mint,
associated_token::authority = owner,
)]
pub position_token_account: Box<Account<'info, TokenAccount>>,
pub whirlpool: Box<Account<'info, Whirlpool>>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
pub associated_token_program: Program<'info, AssociatedToken>,
/// CHECK: checked via account constraints
#[account(address = mpl_token_metadata::ID)]
pub metadata_program: UncheckedAccount<'info>,
/// CHECK: checked via account constraints
#[account(address = WP_NFT_UPDATE_AUTH)]
pub metadata_update_auth: UncheckedAccount<'info>,
}
/*
Opens a new Whirlpool Position with Metadata account.
*/
pub fn handler(
ctx: Context<OpenPositionWithMetadata>,
_bumps: OpenPositionWithMetadataBumps,
tick_lower_index: i32,
tick_upper_index: i32,
) -> ProgramResult {
let whirlpool = &ctx.accounts.whirlpool;
let position_mint = &ctx.accounts.position_mint;
let position = &mut ctx.accounts.position;
position.open_position(
whirlpool,
position_mint.key(),
tick_lower_index,
tick_upper_index,
)?;
mint_position_token_with_metadata_and_remove_authority(
whirlpool,
position_mint,
&ctx.accounts.position_token_account,
&ctx.accounts.position_metadata_account,
&ctx.accounts.metadata_update_auth,
&ctx.accounts.funder,
&ctx.accounts.metadata_program,
&ctx.accounts.token_program,
&ctx.accounts.system_program,
&ctx.accounts.rent,
)
}

View File

@ -0,0 +1,23 @@
use anchor_lang::prelude::*;
use crate::state::WhirlpoolsConfig;
#[derive(Accounts)]
pub struct SetCollectProtocolFeesAuthority<'info> {
#[account(mut)]
pub whirlpools_config: Account<'info, WhirlpoolsConfig>,
#[account(address = whirlpools_config.collect_protocol_fees_authority)]
pub collect_protocol_fees_authority: Signer<'info>,
pub new_collect_protocol_fees_authority: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<SetCollectProtocolFeesAuthority>) -> ProgramResult {
Ok(ctx
.accounts
.whirlpools_config
.update_collect_protocol_fees_authority(
ctx.accounts.new_collect_protocol_fees_authority.key(),
))
}

View File

@ -0,0 +1,24 @@
use anchor_lang::prelude::*;
use crate::state::{FeeTier, WhirlpoolsConfig};
#[derive(Accounts)]
pub struct SetDefaultFeeRate<'info> {
pub whirlpools_config: Account<'info, WhirlpoolsConfig>,
#[account(mut, has_one = whirlpools_config)]
pub fee_tier: Account<'info, FeeTier>,
#[account(address = whirlpools_config.fee_authority)]
pub fee_authority: Signer<'info>,
}
/*
Updates the default fee rate on a FeeTier object.
*/
pub fn handler(ctx: Context<SetDefaultFeeRate>, default_fee_rate: u16) -> ProgramResult {
Ok(ctx
.accounts
.fee_tier
.update_default_fee_rate(default_fee_rate)?)
}

View File

@ -0,0 +1,22 @@
use anchor_lang::prelude::*;
use crate::state::WhirlpoolsConfig;
#[derive(Accounts)]
pub struct SetDefaultProtocolFeeRate<'info> {
#[account(mut)]
pub whirlpools_config: Account<'info, WhirlpoolsConfig>,
#[account(address = whirlpools_config.fee_authority)]
pub fee_authority: Signer<'info>,
}
pub fn handler(
ctx: Context<SetDefaultProtocolFeeRate>,
default_protocol_fee_rate: u16,
) -> ProgramResult {
Ok(ctx
.accounts
.whirlpools_config
.update_default_protocol_fee_rate(default_protocol_fee_rate)?)
}

View File

@ -0,0 +1,22 @@
use anchor_lang::prelude::*;
use crate::state::WhirlpoolsConfig;
#[derive(Accounts)]
pub struct SetFeeAuthority<'info> {
#[account(mut)]
pub whirlpools_config: Account<'info, WhirlpoolsConfig>,
#[account(address = whirlpools_config.fee_authority)]
pub fee_authority: Signer<'info>,
pub new_fee_authority: UncheckedAccount<'info>,
}
/// Set the fee authority. Only the current fee authority has permission to invoke this instruction.
pub fn handler(ctx: Context<SetFeeAuthority>) -> ProgramResult {
Ok(ctx
.accounts
.whirlpools_config
.update_fee_authority(ctx.accounts.new_fee_authority.key()))
}

View File

@ -0,0 +1,18 @@
use anchor_lang::prelude::*;
use crate::state::{Whirlpool, WhirlpoolsConfig};
#[derive(Accounts)]
pub struct SetFeeRate<'info> {
pub whirlpools_config: Account<'info, WhirlpoolsConfig>,
#[account(mut, has_one = whirlpools_config)]
pub whirlpool: Account<'info, Whirlpool>,
#[account(address = whirlpools_config.fee_authority)]
pub fee_authority: Signer<'info>,
}
pub fn handler(ctx: Context<SetFeeRate>, fee_rate: u16) -> ProgramResult {
Ok(ctx.accounts.whirlpool.update_fee_rate(fee_rate)?)
}

View File

@ -0,0 +1,21 @@
use anchor_lang::prelude::*;
use crate::state::{Whirlpool, WhirlpoolsConfig};
#[derive(Accounts)]
pub struct SetProtocolFeeRate<'info> {
pub whirlpools_config: Account<'info, WhirlpoolsConfig>,
#[account(mut, has_one = whirlpools_config)]
pub whirlpool: Account<'info, Whirlpool>,
#[account(address = whirlpools_config.fee_authority)]
pub fee_authority: Signer<'info>,
}
pub fn handler(ctx: Context<SetProtocolFeeRate>, protocol_fee_rate: u16) -> ProgramResult {
Ok(ctx
.accounts
.whirlpool
.update_protocol_fee_rate(protocol_fee_rate)?)
}

View File

@ -0,0 +1,22 @@
use anchor_lang::prelude::*;
use crate::state::Whirlpool;
#[derive(Accounts)]
#[instruction(reward_index: u8)]
pub struct SetRewardAuthority<'info> {
#[account(mut)]
pub whirlpool: Account<'info, Whirlpool>,
#[account(address = whirlpool.reward_infos[reward_index as usize].authority)]
pub reward_authority: Signer<'info>,
pub new_reward_authority: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<SetRewardAuthority>, reward_index: u8) -> ProgramResult {
Ok(ctx.accounts.whirlpool.update_reward_authority(
reward_index as usize,
ctx.accounts.new_reward_authority.key(),
)?)
}

View File

@ -0,0 +1,29 @@
use anchor_lang::prelude::*;
use crate::state::{Whirlpool, WhirlpoolsConfig};
#[derive(Accounts)]
#[instruction(reward_index: u8)]
pub struct SetRewardAuthorityBySuperAuthority<'info> {
pub whirlpools_config: Account<'info, WhirlpoolsConfig>,
#[account(mut, has_one = whirlpools_config)]
pub whirlpool: Account<'info, Whirlpool>,
#[account(address = whirlpools_config.reward_emissions_super_authority)]
pub reward_emissions_super_authority: Signer<'info>,
pub new_reward_authority: UncheckedAccount<'info>,
}
/// Set the whirlpool reward authority at the provided `reward_index`.
/// Only the current reward emissions super authority has permission to invoke this instruction.
pub fn handler(
ctx: Context<SetRewardAuthorityBySuperAuthority>,
reward_index: u8,
) -> ProgramResult {
Ok(ctx.accounts.whirlpool.update_reward_authority(
reward_index as usize,
ctx.accounts.new_reward_authority.key(),
)?)
}

View File

@ -0,0 +1,48 @@
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
use crate::errors::ErrorCode;
use crate::manager::whirlpool_manager::next_whirlpool_reward_infos;
use crate::math::checked_mul_shift_right;
use crate::state::Whirlpool;
use crate::util::to_timestamp_u64;
const DAY_IN_SECONDS: u128 = 60 * 60 * 24;
#[derive(Accounts)]
#[instruction(reward_index: u8)]
pub struct SetRewardEmissions<'info> {
#[account(mut)]
pub whirlpool: Account<'info, Whirlpool>,
#[account(address = whirlpool.reward_infos[reward_index as usize].authority)]
pub reward_authority: Signer<'info>,
#[account(address = whirlpool.reward_infos[reward_index as usize].vault)]
pub reward_vault: Account<'info, TokenAccount>,
}
pub fn handler(
ctx: Context<SetRewardEmissions>,
reward_index: u8,
emissions_per_second_x64: u128,
) -> ProgramResult {
let whirlpool = &ctx.accounts.whirlpool;
let reward_vault = &ctx.accounts.reward_vault;
let emissions_per_day = checked_mul_shift_right(DAY_IN_SECONDS, emissions_per_second_x64)?;
if reward_vault.amount < emissions_per_day {
return Err(ErrorCode::RewardVaultAmountInsufficient.into());
}
let clock = Clock::get()?;
let timestamp = to_timestamp_u64(clock.unix_timestamp)?;
let next_reward_infos = next_whirlpool_reward_infos(whirlpool, timestamp)?;
Ok(ctx.accounts.whirlpool.update_emissions(
reward_index as usize,
next_reward_infos,
timestamp,
emissions_per_second_x64,
)?)
}

View File

@ -0,0 +1,23 @@
use anchor_lang::prelude::*;
use crate::state::WhirlpoolsConfig;
#[derive(Accounts)]
pub struct SetRewardEmissionsSuperAuthority<'info> {
#[account(mut)]
pub whirlpools_config: Account<'info, WhirlpoolsConfig>,
#[account(address = whirlpools_config.reward_emissions_super_authority)]
pub reward_emissions_super_authority: Signer<'info>,
pub new_reward_emissions_super_authority: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<SetRewardEmissionsSuperAuthority>) -> ProgramResult {
Ok(ctx
.accounts
.whirlpools_config
.update_reward_emissions_super_authority(
ctx.accounts.new_reward_emissions_super_authority.key(),
))
}

View File

@ -0,0 +1,172 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};
use crate::{
errors::ErrorCode,
manager::swap_manager::*,
state::{TickArray, Whirlpool},
util::{
to_timestamp_u64, transfer_from_owner_to_vault, transfer_from_vault_to_owner,
SwapTickSequence,
},
};
#[derive(Accounts)]
pub struct Swap<'info> {
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
pub token_authority: Signer<'info>,
#[account(mut)]
pub whirlpool: Box<Account<'info, Whirlpool>>,
#[account(mut, constraint = token_owner_account_a.mint == whirlpool.token_mint_a)]
pub token_owner_account_a: Box<Account<'info, TokenAccount>>,
#[account(mut, address = whirlpool.token_vault_a)]
pub token_vault_a: Box<Account<'info, TokenAccount>>,
#[account(mut, constraint = token_owner_account_b.mint == whirlpool.token_mint_b)]
pub token_owner_account_b: Box<Account<'info, TokenAccount>>,
#[account(mut, address = whirlpool.token_vault_b)]
pub token_vault_b: Box<Account<'info, TokenAccount>>,
#[account(mut, has_one = whirlpool)]
pub tick_array_0: AccountLoader<'info, TickArray>,
#[account(mut, has_one = whirlpool)]
pub tick_array_1: AccountLoader<'info, TickArray>,
#[account(mut, has_one = whirlpool)]
pub tick_array_2: AccountLoader<'info, TickArray>,
#[account(seeds = [b"oracle", whirlpool.key().as_ref()],bump)]
/// Oracle is currently unused and will be enabled on subsequent updates
pub oracle: UncheckedAccount<'info>,
}
pub fn handler(
ctx: Context<Swap>,
amount: u64,
other_amount_threshold: u64,
sqrt_price_limit: u128,
amount_specified_is_input: bool,
a_to_b: bool, // Zero for one
) -> ProgramResult {
let whirlpool = &mut ctx.accounts.whirlpool;
let clock = Clock::get()?;
// Update the global reward growth which increases as a function of time.
let timestamp = to_timestamp_u64(clock.unix_timestamp)?;
let mut swap_tick_sequence = SwapTickSequence::new(
ctx.accounts.tick_array_0.load_mut().unwrap(),
ctx.accounts.tick_array_1.load_mut().ok(),
ctx.accounts.tick_array_2.load_mut().ok(),
);
let swap_update = swap(
&whirlpool,
&mut swap_tick_sequence,
amount,
sqrt_price_limit,
amount_specified_is_input,
a_to_b,
timestamp,
)?;
if amount_specified_is_input {
if (a_to_b && other_amount_threshold > swap_update.amount_b)
|| (!a_to_b && other_amount_threshold > swap_update.amount_a)
{
return Err(ErrorCode::AmountOutBelowMinimum.into());
}
} else {
if (a_to_b && other_amount_threshold < swap_update.amount_a)
|| (!a_to_b && other_amount_threshold < swap_update.amount_b)
{
return Err(ErrorCode::AmountInAboveMaximum.into());
}
}
whirlpool.update_after_swap(
swap_update.next_liquidity,
swap_update.next_tick_index,
swap_update.next_sqrt_price,
swap_update.next_fee_growth_global,
swap_update.next_reward_infos,
swap_update.next_protocol_fee,
a_to_b,
timestamp,
);
perform_swap(
&ctx.accounts.whirlpool,
&ctx.accounts.token_authority,
&ctx.accounts.token_owner_account_a,
&ctx.accounts.token_owner_account_b,
&ctx.accounts.token_vault_a,
&ctx.accounts.token_vault_b,
&ctx.accounts.token_program,
swap_update.amount_a,
swap_update.amount_b,
a_to_b,
)
}
fn perform_swap<'info>(
whirlpool: &Account<'info, Whirlpool>,
token_authority: &Signer<'info>,
token_owner_account_a: &Account<'info, TokenAccount>,
token_owner_account_b: &Account<'info, TokenAccount>,
token_vault_a: &Account<'info, TokenAccount>,
token_vault_b: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
amount_a: u64,
amount_b: u64,
a_to_b: bool,
) -> ProgramResult {
// Transfer from user to pool
let deposit_account_user;
let deposit_account_pool;
let deposit_amount;
// Transfer from pool to user
let withdrawal_account_user;
let withdrawal_account_pool;
let withdrawal_amount;
if a_to_b {
deposit_account_user = token_owner_account_a;
deposit_account_pool = token_vault_a;
deposit_amount = amount_a;
withdrawal_account_user = token_owner_account_b;
withdrawal_account_pool = token_vault_b;
withdrawal_amount = amount_b;
} else {
deposit_account_user = token_owner_account_b;
deposit_account_pool = token_vault_b;
deposit_amount = amount_b;
withdrawal_account_user = token_owner_account_a;
withdrawal_account_pool = token_vault_a;
withdrawal_amount = amount_a;
}
transfer_from_owner_to_vault(
token_authority,
deposit_account_user,
deposit_account_pool,
token_program,
deposit_amount,
)?;
transfer_from_vault_to_owner(
whirlpool,
withdrawal_account_pool,
withdrawal_account_user,
token_program,
withdrawal_amount,
)?;
Ok(())
}

View File

@ -0,0 +1,41 @@
use anchor_lang::prelude::ProgramResult;
use anchor_lang::prelude::*;
use crate::{
manager::liquidity_manager::calculate_fee_and_reward_growths, state::*, util::to_timestamp_u64,
};
#[derive(Accounts)]
pub struct UpdateFeesAndRewards<'info> {
#[account(mut)]
pub whirlpool: Account<'info, Whirlpool>,
#[account(mut, has_one = whirlpool)]
pub position: Account<'info, Position>,
#[account(has_one = whirlpool)]
pub tick_array_lower: AccountLoader<'info, TickArray>,
#[account(has_one = whirlpool)]
pub tick_array_upper: AccountLoader<'info, TickArray>,
}
pub fn handler(ctx: Context<UpdateFeesAndRewards>) -> ProgramResult {
let whirlpool = &mut ctx.accounts.whirlpool;
let position = &mut ctx.accounts.position;
let clock = Clock::get()?;
let timestamp = to_timestamp_u64(clock.unix_timestamp)?;
let (position_update, reward_infos) = calculate_fee_and_reward_growths(
whirlpool,
position,
&ctx.accounts.tick_array_lower,
&ctx.accounts.tick_array_upper,
timestamp,
)?;
whirlpool.update_rewards(reward_infos, timestamp);
position.update(&position_update);
Ok(())
}

View File

@ -0,0 +1,461 @@
//! A concentrated liquidity AMM contract powered by Orca.
use anchor_lang::prelude::*;
declare_id!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc");
#[doc(hidden)]
pub mod constants;
#[doc(hidden)]
pub mod errors;
#[doc(hidden)]
pub mod instructions;
#[doc(hidden)]
pub mod manager;
#[doc(hidden)]
pub mod math;
pub mod state;
#[doc(hidden)]
pub mod tests;
#[doc(hidden)]
pub mod util;
use crate::state::{OpenPositionBumps, OpenPositionWithMetadataBumps, WhirlpoolBumps};
use instructions::*;
#[program]
pub mod whirlpool {
use super::*;
/// Initializes a WhirlpoolsConfig account that hosts info & authorities
/// required to govern a set of Whirlpools.
///
/// # Parameters
/// - `fee_authority` - Authority authorized to initialize fee-tiers and set customs fees.
/// - `collect_protocol_fees_authority` - Authority authorized to collect protocol fees.
/// - `reward_emissions_super_authority` - Authority authorized to set reward authorities in pools.
pub fn initialize_config(
ctx: Context<InitializeConfig>,
fee_authority: Pubkey,
collect_protocol_fees_authority: Pubkey,
reward_emissions_super_authority: Pubkey,
default_protocol_fee_rate: u16,
) -> ProgramResult {
return instructions::initialize_config::handler(
ctx,
fee_authority,
collect_protocol_fees_authority,
reward_emissions_super_authority,
default_protocol_fee_rate,
);
}
/// Initializes a Whirlpool account.
/// Fee rate is set to the default values on the config and supplied fee_tier.
///
/// # Parameters
/// - `bumps` - The bump value when deriving the PDA of the Whirlpool address.
/// - `tick_spacing` - The desired tick spacing for this pool.
/// - `initial_sqrt_price` - The desired initial sqrt-price for this pool
///
/// # Special Errors
/// `InvalidTokenMintOrder` - The order of mints have to be ordered by
/// `SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64
///
pub fn initialize_pool(
ctx: Context<InitializePool>,
bumps: WhirlpoolBumps,
tick_spacing: u16,
initial_sqrt_price: u128,
) -> ProgramResult {
return instructions::initialize_pool::handler(
ctx,
bumps,
tick_spacing,
initial_sqrt_price,
);
}
/// Initializes a tick_array account to represent a tick-range in a Whirlpool.
///
/// # Parameters
/// - `start_tick_index` - The starting tick index for this tick-array.
/// Has to be a multiple of TickArray size & the tick spacing of this pool.
///
/// # Special Errors
/// - `InvalidStartTick` - if the provided start tick is out of bounds or is not a multiple of
/// TICK_ARRAY_SIZE * tick spacing.
pub fn initialize_tick_array(
ctx: Context<InitializeTickArray>,
start_tick_index: i32,
) -> ProgramResult {
return instructions::initialize_tick_array::handler(ctx, start_tick_index);
}
/// Initializes a fee_tier account usable by Whirlpools in a WhirlpoolConfig space.
///
/// # Authority
/// - "fee_authority" - Set authority in the WhirlpoolConfig
///
/// # Parameters
/// - `tick_spacing` - The tick-spacing that this fee-tier suggests the default_fee_rate for.
/// - `default_fee_rate` - The default fee rate that a pool will use if the pool uses this
/// fee tier during initialization.
///
/// # Special Errors
/// - `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
pub fn initialize_fee_tier(
ctx: Context<InitializeFeeTier>,
tick_spacing: u16,
default_fee_rate: u16,
) -> ProgramResult {
return instructions::initialize_fee_tier::handler(ctx, tick_spacing, default_fee_rate);
}
/// Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.
///
/// # Authority
/// - "reward_authority" - assigned authority by the reward_super_authority for the specified
/// reward-index in this Whirlpool
///
/// # Parameters
/// - `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS)
///
/// # Special Errors
/// - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized
/// index in this pool, or exceeds NUM_REWARDS, or
/// all reward slots for this pool has been initialized.
pub fn initialize_reward(ctx: Context<InitializeReward>, reward_index: u8) -> ProgramResult {
return instructions::initialize_reward::handler(ctx, reward_index);
}
/// Set the reward emissions for a reward in a Whirlpool.
///
/// # Authority
/// - "reward_authority" - assigned authority by the reward_super_authority for the specified
/// reward-index in this Whirlpool
///
/// # Parameters
/// - `reward_index` - The reward index (0 <= index <= NUM_REWARDS) that we'd like to modify.
/// - `emissions_per_second_x64` - The amount of rewards emitted in this pool.
///
/// # Special Errors
/// - `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit
/// more than a day of desired emissions.
/// - `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.
/// - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized
/// index in this pool, or exceeds NUM_REWARDS, or
/// all reward slots for this pool has been initialized.
pub fn set_reward_emissions(
ctx: Context<SetRewardEmissions>,
reward_index: u8,
emissions_per_second_x64: u128,
) -> ProgramResult {
return instructions::set_reward_emissions::handler(
ctx,
reward_index,
emissions_per_second_x64,
);
}
/// Open a position in a Whirlpool. A unique token will be minted to represent the position
/// in the users wallet. The position will start off with 0 liquidity.
///
/// # Parameters
/// - `tick_lower_index` - The tick specifying the lower end of the position range.
/// - `tick_upper_index` - The tick specifying the upper end of the position range.
///
/// # Special Errors
/// - `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of
/// the tick-spacing in this pool.
pub fn open_position(
ctx: Context<OpenPosition>,
bumps: OpenPositionBumps,
tick_lower_index: i32,
tick_upper_index: i32,
) -> ProgramResult {
return instructions::open_position::handler(
ctx,
bumps,
tick_lower_index,
tick_upper_index,
);
}
/// Open a position in a Whirlpool. A unique token will be minted to represent the position
/// in the users wallet. Additional Metaplex metadata is appended to identify the token.
/// The position will start off with 0 liquidity.
///
/// # Parameters
/// - `tick_lower_index` - The tick specifying the lower end of the position range.
/// - `tick_upper_index` - The tick specifying the upper end of the position range.
///
/// # Special Errors
/// - `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of
/// the tick-spacing in this pool.
pub fn open_position_with_metadata(
ctx: Context<OpenPositionWithMetadata>,
bumps: OpenPositionWithMetadataBumps,
tick_lower_index: i32,
tick_upper_index: i32,
) -> ProgramResult {
return instructions::open_position_with_metadata::handler(
ctx,
bumps,
tick_lower_index,
tick_upper_index,
);
}
/// Add liquidity to a position in the Whirlpool.
///
/// # Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
///
/// # Parameters
/// - `liquidity_amount` - The total amount of Liquidity the user is willing to deposit.
/// - `token_max_a` - The maximum amount of tokenA the user is willing to deposit.
/// - `token_max_b` - The maximum amount of tokenB the user is willing to deposit.
///
/// # Special Errors
/// - `LiquidityZero` - Provided liquidity amount is zero.
/// - `LiquidityTooHigh` - Provided liquidity exceeds u128::max.
/// - `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount.
pub fn increase_liquidity(
ctx: Context<ModifyLiquidity>,
liquidity_amount: u128,
token_max_a: u64,
token_max_b: u64,
) -> ProgramResult {
return instructions::increase_liquidity::handler(
ctx,
liquidity_amount,
token_max_a,
token_max_b,
);
}
/// Withdraw liquidity from a position in the Whirlpool.
///
/// # Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
///
/// # Parameters
/// - `liquidity_amount` - The total amount of Liquidity the user desires to withdraw.
/// - `token_min_a` - The minimum amount of tokenA the user is willing to withdraw.
/// - `token_min_b` - The minimum amount of tokenB the user is willing to withdraw.
///
/// # Special Errors
/// - `LiquidityZero` - Provided liquidity amount is zero.
/// - `LiquidityTooHigh` - Provided liquidity exceeds u128::max.
/// - `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount.
pub fn decrease_liquidity(
ctx: Context<ModifyLiquidity>,
liquidity_amount: u128,
token_min_a: u64,
token_min_b: u64,
) -> ProgramResult {
return instructions::decrease_liquidity::handler(
ctx,
liquidity_amount,
token_min_a,
token_min_b,
);
}
/// Update the accrued fees and rewards for a position.
///
/// # Special Errors
/// - `TickNotFound` - Provided tick array account does not contain the tick for this position.
/// - `LiquidityZero` - Position has zero liquidity and therefore already has the most updated fees and reward values.
pub fn update_fees_and_rewards(ctx: Context<UpdateFeesAndRewards>) -> ProgramResult {
return instructions::update_fees_and_rewards::handler(ctx);
}
/// Collect fees accrued for this position.
///
/// # Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
pub fn collect_fees(ctx: Context<CollectFees>) -> ProgramResult {
return instructions::collect_fees::handler(ctx);
}
/// Collect rewards accrued for this position.
///
/// # Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
pub fn collect_reward(ctx: Context<CollectReward>, reward_index: u8) -> ProgramResult {
return instructions::collect_reward::handler(ctx, reward_index);
}
/// Collect the protocol fees accrued in this Whirlpool
///
/// # Authority
/// - `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees
pub fn collect_protocol_fees(ctx: Context<CollectProtocolFees>) -> ProgramResult {
return instructions::collect_protocol_fees::handler(ctx);
}
/// Perform a swap in this Whirlpool
///
/// # Parameters
/// - `amount`
/// - `other_amount_threshold`
/// - `sqrt_price_limit`
/// - `exact_input`
/// - `a_to_b`
pub fn swap(
ctx: Context<Swap>,
amount: u64,
other_amount_threshold: u64,
sqrt_price_limit: u128,
exact_input: bool,
a_to_b: bool,
) -> ProgramResult {
return instructions::swap::handler(
ctx,
amount,
other_amount_threshold,
sqrt_price_limit,
exact_input,
a_to_b,
);
}
pub fn close_position(ctx: Context<ClosePosition>) -> ProgramResult {
return instructions::close_position::handler(ctx);
}
/// Set the default_fee_rate for a FeeTier
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// - "fee_authority" - Set authority in the WhirlpoolConfig
///
/// # Parameters
/// - `default_fee_rate` - The default fee rate that a pool will use if the pool uses this
/// fee tier during initialization.
///
/// # Special Errors
/// - `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
pub fn set_default_fee_rate(
ctx: Context<SetDefaultFeeRate>,
default_fee_rate: u16,
) -> ProgramResult {
return instructions::set_default_fee_rate::handler(ctx, default_fee_rate);
}
/// Sets the default protocol fee rate for a WhirlpoolConfig
/// Protocol fee rate is represented as a basis point.
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
///
/// # Parameters
/// - `default_protocol_fee_rate` - Rate that is referenced during the initialization of a Whirlpool using this config.
///
/// # Special Errors
/// - `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE.
pub fn set_default_protocol_fee_rate(
ctx: Context<SetDefaultProtocolFeeRate>,
default_protocol_fee_rate: u16,
) -> ProgramResult {
return instructions::set_default_protocol_fee_rate::handler(
ctx,
default_protocol_fee_rate,
);
}
/// Sets the fee rate for a Whirlpool.
/// Fee rate is represented as hundredths of a basis point.
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
///
/// # Parameters
/// - `fee_rate` - The rate that the pool will use to calculate fees going onwards.
///
/// # Special Errors
/// - `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
pub fn set_fee_rate(ctx: Context<SetFeeRate>, fee_rate: u16) -> ProgramResult {
return instructions::set_fee_rate::handler(ctx, fee_rate);
}
/// Sets the protocol fee rate for a Whirlpool.
/// Protocol fee rate is represented as a basis point.
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
///
/// # Parameters
/// - `protocol_fee_rate` - The rate that the pool will use to calculate protocol fees going onwards.
///
/// # Special Errors
/// - `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE.
pub fn set_protocol_fee_rate(
ctx: Context<SetProtocolFeeRate>,
protocol_fee_rate: u16,
) -> ProgramResult {
return instructions::set_protocol_fee_rate::handler(ctx, protocol_fee_rate);
}
/// Sets the fee authority for a WhirlpoolConfig.
/// The fee authority can set the fee & protocol fee rate for individual pools or
/// set the default fee rate for newly minted pools.
/// Only the current collect fee authority has permission to invoke this instruction.
///
/// # Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
pub fn set_fee_authority(ctx: Context<SetFeeAuthority>) -> ProgramResult {
return instructions::set_fee_authority::handler(ctx);
}
/// Sets the fee authority to collect protocol fees for a WhirlpoolConfig.
/// Only the current collect protocol fee authority has permission to invoke this instruction.
///
/// # Authority
/// - "fee_authority" - Set authority that can collect protocol fees in the WhirlpoolConfig
pub fn set_collect_protocol_fees_authority(
ctx: Context<SetCollectProtocolFeesAuthority>,
) -> ProgramResult {
return instructions::set_collect_protocol_fees_authority::handler(ctx);
}
/// Set the whirlpool reward authority at the provided `reward_index`.
/// Only the current reward authority for this reward index has permission to invoke this instruction.
///
/// # Authority
/// - "reward_authority" - Set authority that can control reward emission for this particular reward.
pub fn set_reward_authority(
ctx: Context<SetRewardAuthority>,
reward_index: u8,
) -> ProgramResult {
return instructions::set_reward_authority::handler(ctx, reward_index);
}
/// Set the whirlpool reward authority at the provided `reward_index`.
/// Only the current reward super authority has permission to invoke this instruction.
///
/// # Authority
/// - "reward_authority" - Set authority that can control reward emission for this particular reward.
pub fn set_reward_authority_by_super_authority(
ctx: Context<SetRewardAuthorityBySuperAuthority>,
reward_index: u8,
) -> ProgramResult {
return instructions::set_reward_authority_by_super_authority::handler(ctx, reward_index);
}
/// Set the whirlpool reward super authority for a WhirlpoolConfig
/// Only the current reward super authority has permission to invoke this instruction.
/// This instruction will not change the authority on any `WhirlpoolRewardInfo` whirlpool rewards.
///
/// # Authority
/// - "reward_emissions_super_authority" - Set authority that can control reward authorities for all pools in this config space.
pub fn set_reward_emissions_super_authority(
ctx: Context<SetRewardEmissionsSuperAuthority>,
) -> ProgramResult {
return instructions::set_reward_emissions_super_authority::handler(ctx);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
pub mod liquidity_manager;
pub mod position_manager;
pub mod swap_manager;
pub mod tick_manager;
pub mod whirlpool_manager;

View File

@ -0,0 +1,322 @@
use crate::{
errors::ErrorCode,
math::{add_liquidity_delta, checked_mul_shift_right},
state::{Position, PositionUpdate, NUM_REWARDS},
};
pub fn next_position_modify_liquidity_update(
position: &Position,
liquidity_delta: i128,
fee_growth_inside_a: u128,
fee_growth_inside_b: u128,
reward_growths_inside: &[u128; NUM_REWARDS],
) -> Result<PositionUpdate, ErrorCode> {
let mut update = PositionUpdate::default();
// Calculate fee deltas.
// If fee deltas overflow, default to a zero value. This means the position loses
// all fees earned since the last time the position was modified or fees collected.
let growth_delta_a = fee_growth_inside_a.wrapping_sub(position.fee_growth_checkpoint_a);
let fee_delta_a = checked_mul_shift_right(position.liquidity, growth_delta_a).unwrap_or(0);
let growth_delta_b = fee_growth_inside_b.wrapping_sub(position.fee_growth_checkpoint_b);
let fee_delta_b = checked_mul_shift_right(position.liquidity, growth_delta_b).unwrap_or(0);
update.fee_growth_checkpoint_a = fee_growth_inside_a;
update.fee_growth_checkpoint_b = fee_growth_inside_b;
// Overflows allowed. Must collect fees owed before overflow.
update.fee_owed_a = position.fee_owed_a.wrapping_add(fee_delta_a);
update.fee_owed_b = position.fee_owed_b.wrapping_add(fee_delta_b);
for i in 0..NUM_REWARDS {
let reward_growth_inside = reward_growths_inside[i];
let curr_reward_info = position.reward_infos[i];
// Calculate reward delta.
// If reward delta overflows, default to a zero value. This means the position loses all
// rewards earned since the last time the position was modified or rewards were collected.
let reward_growth_delta =
reward_growth_inside.wrapping_sub(curr_reward_info.growth_inside_checkpoint);
let amount_owed_delta =
checked_mul_shift_right(position.liquidity, reward_growth_delta).unwrap_or(0);
update.reward_infos[i].growth_inside_checkpoint = reward_growth_inside;
// Overflows allowed. Must collect rewards owed before overflow.
update.reward_infos[i].amount_owed =
curr_reward_info.amount_owed.wrapping_add(amount_owed_delta);
}
update.liquidity = add_liquidity_delta(position.liquidity, liquidity_delta)?;
Ok(update)
}
#[cfg(test)]
mod position_manager_unit_tests {
use crate::{
math::{add_liquidity_delta, Q64_RESOLUTION},
state::{position_builder::PositionBuilder, Position, PositionRewardInfo, NUM_REWARDS},
};
use super::next_position_modify_liquidity_update;
#[test]
fn ok_positive_liquidity_delta_fee_growth() {
let position = PositionBuilder::new(-10, 10)
.liquidity(0)
.fee_owed_a(10)
.fee_owed_b(500)
.fee_growth_checkpoint_a(100 << Q64_RESOLUTION)
.fee_growth_checkpoint_b(100 << Q64_RESOLUTION)
.build();
let update = next_position_modify_liquidity_update(
&position,
1000,
1000 << Q64_RESOLUTION,
2000 << Q64_RESOLUTION,
&[0, 0, 0],
)
.unwrap();
assert_eq!(update.liquidity, 1000);
assert_eq!(update.fee_growth_checkpoint_a, 1000 << Q64_RESOLUTION);
assert_eq!(update.fee_growth_checkpoint_b, 2000 << Q64_RESOLUTION);
assert_eq!(update.fee_owed_a, 10);
assert_eq!(update.fee_owed_b, 500);
for i in 0..NUM_REWARDS {
assert_eq!(update.reward_infos[i].amount_owed, 0);
assert_eq!(update.reward_infos[i].growth_inside_checkpoint, 0);
}
}
#[test]
fn ok_negative_liquidity_delta_fee_growth() {
let position = PositionBuilder::new(-10, 10)
.liquidity(10000)
.fee_growth_checkpoint_a(100 << Q64_RESOLUTION)
.fee_growth_checkpoint_b(100 << Q64_RESOLUTION)
.build();
let update = next_position_modify_liquidity_update(
&position,
-5000,
120 << Q64_RESOLUTION,
250 << Q64_RESOLUTION,
&[0, 0, 0],
)
.unwrap();
assert_eq!(update.liquidity, 5000);
assert_eq!(update.fee_growth_checkpoint_a, 120 << Q64_RESOLUTION);
assert_eq!(update.fee_growth_checkpoint_b, 250 << Q64_RESOLUTION);
assert_eq!(update.fee_owed_a, 200_000);
assert_eq!(update.fee_owed_b, 1500_000);
for i in 0..NUM_REWARDS {
assert_eq!(update.reward_infos[i].amount_owed, 0);
assert_eq!(update.reward_infos[i].growth_inside_checkpoint, 0);
}
}
#[test]
#[should_panic(expected = "LiquidityUnderflow")]
fn liquidity_underflow() {
let position = PositionBuilder::new(-10, 10).build();
next_position_modify_liquidity_update(&position, -100, 0, 0, &[0, 0, 0]).unwrap();
}
#[test]
#[should_panic(expected = "LiquidityOverflow")]
fn liquidity_overflow() {
let position = PositionBuilder::new(-10, 10).liquidity(u128::MAX).build();
next_position_modify_liquidity_update(&position, i128::MAX, 0, 0, &[0, 0, 0]).unwrap();
}
#[test]
fn fee_delta_overflow_defaults_zero() {
let position = PositionBuilder::new(-10, 10)
.liquidity(i64::MAX as u128)
.fee_owed_a(10)
.fee_owed_b(20)
.build();
let update = next_position_modify_liquidity_update(
&position,
i64::MAX as i128,
u128::MAX,
u128::MAX,
&[0, 0, 0],
)
.unwrap();
assert_eq!(update.fee_growth_checkpoint_a, u128::MAX);
assert_eq!(update.fee_growth_checkpoint_b, u128::MAX);
assert_eq!(update.fee_owed_a, 10);
assert_eq!(update.fee_owed_b, 20);
}
#[test]
fn ok_reward_growth() {
struct Test<'a> {
name: &'a str,
position: &'a Position,
liquidity_delta: i128,
reward_growths_inside: [u128; NUM_REWARDS],
expected_reward_infos: [PositionRewardInfo; NUM_REWARDS],
}
let position = &PositionBuilder::new(-10, 10)
.liquidity(2500)
.reward_infos([
PositionRewardInfo {
growth_inside_checkpoint: 100 << Q64_RESOLUTION,
amount_owed: 50,
},
PositionRewardInfo {
growth_inside_checkpoint: 250 << Q64_RESOLUTION,
amount_owed: 100,
},
PositionRewardInfo {
growth_inside_checkpoint: 10 << Q64_RESOLUTION,
amount_owed: 0,
},
])
.build();
for test in [
Test {
name: "all initialized reward growths update",
position,
liquidity_delta: 2500,
reward_growths_inside: [
200 << Q64_RESOLUTION,
500 << Q64_RESOLUTION,
1000 << Q64_RESOLUTION,
],
expected_reward_infos: [
PositionRewardInfo {
growth_inside_checkpoint: 200 << Q64_RESOLUTION,
amount_owed: 250_050,
},
PositionRewardInfo {
growth_inside_checkpoint: 500 << Q64_RESOLUTION,
amount_owed: 625_100,
},
PositionRewardInfo {
growth_inside_checkpoint: 1000 << Q64_RESOLUTION,
amount_owed: 2_475_000,
},
],
},
Test {
name: "reward delta overflow defaults to zero",
position: &PositionBuilder::new(-10, 10)
.liquidity(i64::MAX as u128)
.reward_infos([
PositionRewardInfo {
..Default::default()
},
PositionRewardInfo {
amount_owed: 100,
..Default::default()
},
PositionRewardInfo {
amount_owed: 200,
..Default::default()
},
])
.build(),
liquidity_delta: 2500,
reward_growths_inside: [u128::MAX, 500 << Q64_RESOLUTION, 1000 << Q64_RESOLUTION],
expected_reward_infos: [
PositionRewardInfo {
growth_inside_checkpoint: u128::MAX,
amount_owed: 0,
},
PositionRewardInfo {
growth_inside_checkpoint: 500 << Q64_RESOLUTION,
amount_owed: 100,
},
PositionRewardInfo {
growth_inside_checkpoint: 1000 << Q64_RESOLUTION,
amount_owed: 200,
},
],
},
] {
let update = next_position_modify_liquidity_update(
&test.position,
test.liquidity_delta,
0,
0,
&test.reward_growths_inside,
)
.unwrap();
assert_eq!(
update.liquidity,
add_liquidity_delta(test.position.liquidity, test.liquidity_delta).unwrap(),
"{} - assert liquidity delta",
test.name,
);
for i in 0..NUM_REWARDS {
assert_eq!(
update.reward_infos[i].growth_inside_checkpoint,
test.expected_reward_infos[i].growth_inside_checkpoint,
"{} - assert growth_inside_checkpoint",
test.name,
);
assert_eq!(
update.reward_infos[i].amount_owed, test.expected_reward_infos[i].amount_owed,
"{} - assert amount_owed",
test.name
);
}
}
}
#[test]
fn reward_delta_overflow_defaults_zero() {
let position = PositionBuilder::new(-10, 10)
.liquidity(i64::MAX as u128)
.reward_infos([
PositionRewardInfo {
growth_inside_checkpoint: 100,
amount_owed: 1000,
},
PositionRewardInfo {
growth_inside_checkpoint: 100,
amount_owed: 1000,
},
PositionRewardInfo {
growth_inside_checkpoint: 100,
amount_owed: 1000,
},
])
.build();
let update = next_position_modify_liquidity_update(
&position,
i64::MAX as i128,
0,
0,
&[u128::MAX, u128::MAX, u128::MAX],
)
.unwrap();
assert_eq!(
update.reward_infos,
[
PositionRewardInfo {
growth_inside_checkpoint: u128::MAX,
amount_owed: 1000,
},
PositionRewardInfo {
growth_inside_checkpoint: u128::MAX,
amount_owed: 1000,
},
PositionRewardInfo {
growth_inside_checkpoint: u128::MAX,
amount_owed: 1000,
},
]
)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,866 @@
use crate::{
errors::ErrorCode,
math::add_liquidity_delta,
state::{Tick, TickUpdate, WhirlpoolRewardInfo, NUM_REWARDS},
};
pub fn next_tick_cross_update(
tick: &Tick,
fee_growth_global_a: u128,
fee_growth_global_b: u128,
reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS],
) -> Result<TickUpdate, ErrorCode> {
let mut update = TickUpdate::from(tick);
update.fee_growth_outside_a = fee_growth_global_a.wrapping_sub(tick.fee_growth_outside_a);
update.fee_growth_outside_b = fee_growth_global_b.wrapping_sub(tick.fee_growth_outside_b);
for i in 0..NUM_REWARDS {
if !reward_infos[i].initialized() {
continue;
}
update.reward_growths_outside[i] = reward_infos[i]
.growth_global_x64
.wrapping_sub(tick.reward_growths_outside[i]);
}
Ok(update)
}
pub fn next_tick_modify_liquidity_update(
tick: &Tick,
tick_index: i32,
tick_current_index: i32,
fee_growth_global_a: u128,
fee_growth_global_b: u128,
reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS],
liquidity_delta: i128,
is_upper_tick: bool,
) -> Result<TickUpdate, ErrorCode> {
// noop if there is no change in liquidity
if liquidity_delta == 0 {
return Ok(TickUpdate::from(tick));
}
let liquidity_gross = add_liquidity_delta(tick.liquidity_gross, liquidity_delta)?;
// Update to an uninitialized tick if remaining liquidity is being removed
if liquidity_gross == 0 {
return Ok(TickUpdate::default());
}
let (fee_growth_outside_a, fee_growth_outside_b, reward_growths_outside) =
if tick.liquidity_gross == 0 {
// By convention, assume all prior growth happened below the tick
if tick_current_index >= tick_index {
(
fee_growth_global_a,
fee_growth_global_b,
WhirlpoolRewardInfo::to_reward_growths(reward_infos),
)
} else {
(0, 0, [0; NUM_REWARDS])
}
} else {
(
tick.fee_growth_outside_a,
tick.fee_growth_outside_b,
tick.reward_growths_outside,
)
};
let liquidity_net = if is_upper_tick {
tick.liquidity_net
.checked_sub(liquidity_delta)
.ok_or(ErrorCode::LiquidityNetError)?
} else {
tick.liquidity_net
.checked_add(liquidity_delta)
.ok_or(ErrorCode::LiquidityNetError)?
};
Ok(TickUpdate {
initialized: true,
liquidity_net,
liquidity_gross,
fee_growth_outside_a,
fee_growth_outside_b,
reward_growths_outside,
})
}
// Calculates the fee growths inside of tick_lower and tick_upper based on their
// index relative to tick_current_index.
pub fn next_fee_growths_inside(
tick_current_index: i32,
tick_lower: &Tick,
tick_lower_index: i32,
tick_upper: &Tick,
tick_upper_index: i32,
fee_growth_global_a: u128,
fee_growth_global_b: u128,
) -> (u128, u128) {
// By convention, when initializing a tick, all fees have been earned below the tick.
let (fee_growth_below_a, fee_growth_below_b) = if !tick_lower.initialized {
(fee_growth_global_a, fee_growth_global_b)
} else if tick_current_index < tick_lower_index {
(
fee_growth_global_a.wrapping_sub(tick_lower.fee_growth_outside_a),
fee_growth_global_b.wrapping_sub(tick_lower.fee_growth_outside_b),
)
} else {
(
tick_lower.fee_growth_outside_a,
tick_lower.fee_growth_outside_b,
)
};
// By convention, when initializing a tick, no fees have been earned above the tick.
let (fee_growth_above_a, fee_growth_above_b) = if !tick_upper.initialized {
(0, 0)
} else if tick_current_index < tick_upper_index {
(
tick_upper.fee_growth_outside_a,
tick_upper.fee_growth_outside_b,
)
} else {
(
fee_growth_global_a.wrapping_sub(tick_upper.fee_growth_outside_a),
fee_growth_global_b.wrapping_sub(tick_upper.fee_growth_outside_b),
)
};
(
fee_growth_global_a
.wrapping_sub(fee_growth_below_a)
.wrapping_sub(fee_growth_above_a),
fee_growth_global_b
.wrapping_sub(fee_growth_below_b)
.wrapping_sub(fee_growth_above_b),
)
}
// Calculates the reward growths inside of tick_lower and tick_upper based on their positions
// relative to tick_current_index. An uninitialized reward will always have a reward growth of zero.
pub fn next_reward_growths_inside(
tick_current_index: i32,
tick_lower: &Tick,
tick_lower_index: i32,
tick_upper: &Tick,
tick_upper_index: i32,
reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS],
) -> ([u128; NUM_REWARDS]) {
let mut reward_growths_inside = [0; NUM_REWARDS];
for i in 0..NUM_REWARDS {
if !reward_infos[i].initialized() {
continue;
}
// By convention, assume all prior growth happened below the tick
let reward_growths_below = if !tick_lower.initialized {
reward_infos[i].growth_global_x64
} else if tick_current_index < tick_lower_index {
reward_infos[i]
.growth_global_x64
.wrapping_sub(tick_lower.reward_growths_outside[i])
} else {
tick_lower.reward_growths_outside[i]
};
// By convention, assume all prior growth happened below the tick, not above
let reward_growths_above = if !tick_upper.initialized {
0
} else if tick_current_index < tick_upper_index {
tick_upper.reward_growths_outside[i]
} else {
reward_infos[i]
.growth_global_x64
.wrapping_sub(tick_upper.reward_growths_outside[i])
};
reward_growths_inside[i] = reward_infos[i]
.growth_global_x64
.wrapping_sub(reward_growths_below)
.wrapping_sub(reward_growths_above);
}
reward_growths_inside
}
#[cfg(test)]
mod tick_manager_tests {
use anchor_lang::prelude::Pubkey;
use crate::{
errors::ErrorCode,
manager::tick_manager::{
next_fee_growths_inside, next_tick_cross_update, next_tick_modify_liquidity_update,
TickUpdate,
},
math::Q64_RESOLUTION,
state::{tick_builder::TickBuilder, Tick, WhirlpoolRewardInfo, NUM_REWARDS},
};
use super::next_reward_growths_inside;
fn create_test_whirlpool_reward_info(
emissions_per_second_x64: u128,
growth_global_x64: u128,
initialized: bool,
) -> WhirlpoolRewardInfo {
WhirlpoolRewardInfo {
mint: if initialized {
Pubkey::new_unique()
} else {
Pubkey::default()
},
emissions_per_second_x64,
growth_global_x64,
..Default::default()
}
}
#[test]
fn test_next_fee_growths_inside() {
struct Test<'a> {
name: &'a str,
tick_current_index: i32,
tick_lower: Tick,
tick_lower_index: i32,
tick_upper: Tick,
tick_upper_index: i32,
fee_growth_global_a: u128,
fee_growth_global_b: u128,
expected_fee_growths_inside: (u128, u128),
}
for test in [
Test {
name: "current tick index below ticks",
tick_current_index: -200,
tick_lower: Tick {
initialized: true,
fee_growth_outside_a: 2000,
fee_growth_outside_b: 1000,
..Default::default()
},
tick_lower_index: -100,
tick_upper: Tick {
initialized: true,
fee_growth_outside_a: 1000,
fee_growth_outside_b: 1000,
..Default::default()
},
tick_upper_index: 100,
fee_growth_global_a: 3000,
fee_growth_global_b: 3000,
expected_fee_growths_inside: (1000, 0),
},
Test {
name: "current tick index between ticks",
tick_current_index: -20,
tick_lower: Tick {
initialized: true,
fee_growth_outside_a: 2000,
fee_growth_outside_b: 1000,
..Default::default()
},
tick_lower_index: -20,
tick_upper: Tick {
initialized: true,
fee_growth_outside_a: 1500,
fee_growth_outside_b: 1000,
..Default::default()
},
tick_upper_index: 100,
fee_growth_global_a: 4000,
fee_growth_global_b: 3000,
expected_fee_growths_inside: (500, 1000),
},
Test {
name: "current tick index above ticks",
tick_current_index: 200,
tick_lower: Tick {
initialized: true,
fee_growth_outside_a: 2000,
fee_growth_outside_b: 1000,
..Default::default()
},
tick_lower_index: -100,
tick_upper: Tick {
initialized: true,
fee_growth_outside_a: 2500,
fee_growth_outside_b: 2000,
..Default::default()
},
tick_upper_index: 100,
fee_growth_global_a: 3000,
fee_growth_global_b: 3000,
expected_fee_growths_inside: (500, 1000),
},
] {
// System under test
let (fee_growth_inside_a, fee_growth_inside_b) = next_fee_growths_inside(
test.tick_current_index,
&test.tick_lower,
test.tick_lower_index,
&test.tick_upper,
test.tick_upper_index,
test.fee_growth_global_a,
test.fee_growth_global_b,
);
assert_eq!(
fee_growth_inside_a, test.expected_fee_growths_inside.0,
"{} - fee_growth_inside_a",
test.name
);
assert_eq!(
fee_growth_inside_b, test.expected_fee_growths_inside.1,
"{} - fee_growth_inside_b",
test.name
);
}
}
#[test]
fn test_next_reward_growths_inside() {
struct Test<'a> {
name: &'a str,
tick_current_index: i32,
tick_lower: Tick,
tick_lower_index: i32,
tick_upper: Tick,
tick_upper_index: i32,
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
expected_reward_growths_inside: [u128; NUM_REWARDS],
}
for test in [
Test {
name: "current tick index below ticks zero rewards",
tick_lower: Tick {
initialized: true,
reward_growths_outside: [100, 666, 69420],
..Default::default()
},
tick_lower_index: -100,
tick_upper: Tick {
initialized: true,
reward_growths_outside: [100, 666, 69420],
..Default::default()
},
tick_upper_index: 100,
tick_current_index: -200,
reward_infos: [
create_test_whirlpool_reward_info(1, 500, true),
create_test_whirlpool_reward_info(1, 1000, true),
create_test_whirlpool_reward_info(1, 70000, true),
],
expected_reward_growths_inside: [0, 0, 0],
},
Test {
name: "current tick index between ticks",
tick_lower: Tick {
initialized: true,
reward_growths_outside: [200, 134, 480],
..Default::default()
},
tick_lower_index: -100,
tick_upper: Tick {
initialized: true,
reward_growths_outside: [100, 666, 69420],
..Default::default()
},
tick_upper_index: 100,
tick_current_index: 10,
reward_infos: [
create_test_whirlpool_reward_info(1, 1000, true),
create_test_whirlpool_reward_info(1, 2000, true),
create_test_whirlpool_reward_info(1, 80000, true),
],
expected_reward_growths_inside: [700, 1200, 10100],
},
Test {
name: "current tick index above ticks",
tick_lower: Tick {
reward_growths_outside: [200, 134, 480],
initialized: true,
..Default::default()
},
tick_lower_index: -100,
tick_upper: Tick {
initialized: true,
reward_growths_outside: [900, 1334, 10580],
..Default::default()
},
tick_upper_index: 100,
tick_current_index: 250,
reward_infos: [
create_test_whirlpool_reward_info(1, 1000, true),
create_test_whirlpool_reward_info(1, 2000, true),
create_test_whirlpool_reward_info(1, 80000, true),
],
expected_reward_growths_inside: [700, 1200, 10100],
},
Test {
name: "uninitialized rewards no-op",
tick_lower: Tick {
initialized: true,
reward_growths_outside: [200, 134, 480],
..Default::default()
},
tick_lower_index: -100,
tick_upper: Tick {
initialized: true,
reward_growths_outside: [900, 1334, 10580],
..Default::default()
},
tick_upper_index: 100,
tick_current_index: 250,
reward_infos: [
create_test_whirlpool_reward_info(1, 1000, true),
create_test_whirlpool_reward_info(1, 2000, false),
create_test_whirlpool_reward_info(1, 80000, false),
],
expected_reward_growths_inside: [700, 0, 0],
},
] {
// System under test
let results = next_reward_growths_inside(
test.tick_current_index,
&test.tick_lower,
test.tick_lower_index,
&test.tick_upper,
test.tick_upper_index,
&test.reward_infos,
);
for i in 0..NUM_REWARDS {
assert_eq!(
results[i], test.expected_reward_growths_inside[i],
"[{}] {} - reward growth value not equal",
i, test.name
);
assert_eq!(
results[i], test.expected_reward_growths_inside[i],
"[{}] {} - reward growth initialized flag not equal",
i, test.name
);
}
}
}
#[test]
fn test_next_tick_modify_liquidity_update() {
#[derive(Default)]
struct Test<'a> {
name: &'a str,
tick: Tick,
tick_index: i32,
tick_current_index: i32,
fee_growth_global_a: u128,
fee_growth_global_b: u128,
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
liquidity_delta: i128,
is_upper_tick: bool,
expected_update: TickUpdate,
}
// Whirlpool rewards re-used in the tests
let reward_infos = [
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1 << Q64_RESOLUTION,
growth_global_x64: 100 << Q64_RESOLUTION,
..Default::default()
},
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1 << Q64_RESOLUTION,
growth_global_x64: 100 << Q64_RESOLUTION,
..Default::default()
},
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1 << Q64_RESOLUTION,
growth_global_x64: 100 << Q64_RESOLUTION,
..Default::default()
},
];
for test in [
Test {
name: "initialize lower tick with +liquidity, current < tick.index, growths not set",
tick: Tick::default(),
tick_index: 200,
tick_current_index: 100,
liquidity_delta: 42069,
is_upper_tick: false,
fee_growth_global_a: 100,
fee_growth_global_b: 100,
reward_infos,
expected_update: TickUpdate {
initialized: true,
liquidity_net: 42069,
liquidity_gross: 42069,
..Default::default()
},
},
Test {
name: "initialize lower tick with +liquidity, current >= tick.index, growths get set",
tick: Tick::default(),
tick_index: 200,
tick_current_index: 300,
liquidity_delta: 42069,
is_upper_tick: false,
fee_growth_global_a: 100,
fee_growth_global_b: 100,
reward_infos,
expected_update: TickUpdate {
initialized: true,
liquidity_net: 42069,
liquidity_gross: 42069,
fee_growth_outside_a: 100,
fee_growth_outside_b: 100,
reward_growths_outside: [
100 << Q64_RESOLUTION,
100 << Q64_RESOLUTION,
100 << Q64_RESOLUTION,
],
},
..Default::default()
},
Test {
name: "lower tick +liquidity already initialized, growths not set",
tick: TickBuilder::default()
.initialized(true)
.liquidity_net(100)
.liquidity_gross(100)
.build(),
tick_index: 200,
tick_current_index: 100,
liquidity_delta: 42069,
is_upper_tick: false,
fee_growth_global_a: 100,
fee_growth_global_b: 100,
reward_infos,
expected_update: TickUpdate {
initialized: true,
liquidity_net: 42169,
liquidity_gross: 42169,
..Default::default()
},
..Default::default()
},
Test {
name: "upper tick +liquidity already initialized, growths not set, liquidity net should be subtracted",
tick: TickBuilder::default()
.initialized(true)
.liquidity_net(100000)
.liquidity_gross(100000)
.build(),
tick_index: 200,
tick_current_index: 100,
liquidity_delta: 42069,
is_upper_tick: true,
expected_update: TickUpdate {
initialized: true,
liquidity_net:57931,
liquidity_gross: 142069,
..Default::default()
},
..Default::default()
},
Test {
name: "upper tick -liquidity, growths not set, uninitialize tick",
tick: TickBuilder::default()
.initialized(true)
.liquidity_net(-100000)
.liquidity_gross(100000)
.build(),
tick_index: 200,
tick_current_index: 100,
liquidity_delta: -100000,
is_upper_tick: true,
expected_update: TickUpdate {
initialized: false,
liquidity_net: 0,
liquidity_gross: 0,
..Default::default()
},
..Default::default()
},
Test {
name: "lower tick -liquidity, growths not set, initialized no change",
tick: TickBuilder::default()
.initialized(true)
.liquidity_net(100000)
.liquidity_gross(200000)
.build(),
tick_index: 200,
tick_current_index: 100,
liquidity_delta: -100000,
is_upper_tick: false,
expected_update: TickUpdate {
initialized: true,
liquidity_net: 0,
liquidity_gross: 100000,
..Default::default()
},
..Default::default()
},
Test {
name: "liquidity delta zero is no-op",
tick: TickBuilder::default()
.initialized(true)
.liquidity_net(100000)
.liquidity_gross(200000)
.build(),
tick_index: 200,
tick_current_index: 100,
liquidity_delta: 0,
is_upper_tick: false,
expected_update: TickUpdate {
initialized: true,
liquidity_net: 100000,
liquidity_gross: 200000,
..Default::default()
},
..Default::default()
},
Test {
name: "uninitialized rewards get set to zero values",
tick: TickBuilder::default()
.initialized(true)
.reward_growths_outside([100, 200, 50])
.build(),
tick_index: 200,
tick_current_index: 1000,
liquidity_delta: 42069,
is_upper_tick: false,
fee_growth_global_a: 100,
fee_growth_global_b: 100,
reward_infos: [
WhirlpoolRewardInfo{
..Default::default()
},
WhirlpoolRewardInfo{
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1,
growth_global_x64: 250,
..Default::default()
},
WhirlpoolRewardInfo{
..Default::default()
}
],
expected_update: TickUpdate {
initialized: true,
fee_growth_outside_a: 100,
fee_growth_outside_b: 100,
liquidity_net: 42069,
liquidity_gross: 42069,
reward_growths_outside: [0, 250, 0],
..Default::default()
},
}
] {
// System under test
let update = next_tick_modify_liquidity_update(
&test.tick,
test.tick_index,
test.tick_current_index,
test.fee_growth_global_a,
test.fee_growth_global_b,
&test.reward_infos,
test.liquidity_delta,
test.is_upper_tick,
)
.unwrap();
assert_eq!(
update.initialized, test.expected_update.initialized,
"{}: initialized invalid",
test.name
);
assert_eq!(
update.liquidity_net, test.expected_update.liquidity_net,
"{}: liquidity_net invalid",
test.name
);
assert_eq!(
update.liquidity_gross, test.expected_update.liquidity_gross,
"{}: liquidity_gross invalid",
test.name
);
assert_eq!(
update.fee_growth_outside_a, test.expected_update.fee_growth_outside_a,
"{}: fee_growth_outside_a invalid",
test.name
);
assert_eq!(
update.fee_growth_outside_b, test.expected_update.fee_growth_outside_b,
"{}: fee_growth_outside_b invalid",
test.name
);
assert_eq!(
update.reward_growths_outside, test.expected_update.reward_growths_outside,
"{}: reward_growths_outside invalid",
test.name
);
}
}
#[test]
fn test_next_tick_modify_liquidity_update_errors() {
struct Test<'a> {
name: &'a str,
tick: Tick,
tick_index: i32,
tick_current_index: i32,
liquidity_delta: i128,
is_upper_tick: bool,
expected_error: ErrorCode,
}
for test in [
Test {
name: "liquidity gross overflow",
tick: TickBuilder::default().liquidity_gross(u128::MAX).build(),
tick_index: 0,
tick_current_index: 10,
liquidity_delta: i128::MAX,
is_upper_tick: false,
expected_error: ErrorCode::LiquidityOverflow,
},
Test {
name: "liquidity gross underflow",
tick: Tick::default(),
tick_index: 0,
tick_current_index: 10,
liquidity_delta: -100,
is_upper_tick: false,
expected_error: ErrorCode::LiquidityUnderflow,
},
Test {
name: "liquidity net overflow from subtracting negative delta",
tick: TickBuilder::default()
.liquidity_gross(i128::MAX as u128)
.liquidity_net(i128::MAX)
.build(),
tick_index: 0,
tick_current_index: 10,
liquidity_delta: -(i128::MAX - 1),
is_upper_tick: true,
expected_error: ErrorCode::LiquidityNetError,
},
Test {
name: "liquidity net underflow",
tick: TickBuilder::default()
.liquidity_gross(10000)
.liquidity_net(i128::MAX)
.build(),
tick_index: 0,
tick_current_index: 10,
liquidity_delta: i128::MAX,
is_upper_tick: false,
expected_error: ErrorCode::LiquidityNetError,
},
] {
// System under test
let err = next_tick_modify_liquidity_update(
&test.tick,
test.tick_index,
test.tick_current_index,
0,
0,
&[WhirlpoolRewardInfo::default(); NUM_REWARDS],
test.liquidity_delta,
test.is_upper_tick,
)
.unwrap_err();
assert_eq!(err, test.expected_error, "{}", test.name);
}
}
#[test]
fn test_next_tick_cross_update() {
struct Test<'a> {
name: &'a str,
tick: Tick,
fee_growth_global_a: u128,
fee_growth_global_b: u128,
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
expected_update: TickUpdate,
}
for test in [Test {
name: "growths set properly (inverted)",
tick: TickBuilder::default()
.fee_growth_outside_a(1000)
.fee_growth_outside_b(1000)
.reward_growths_outside([500, 250, 100])
.build(),
fee_growth_global_a: 2500,
fee_growth_global_b: 6750,
reward_infos: [
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1,
growth_global_x64: 1000,
..Default::default()
},
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1,
growth_global_x64: 1000,
..Default::default()
},
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1,
growth_global_x64: 1000,
..Default::default()
},
],
expected_update: TickUpdate {
fee_growth_outside_a: 1500,
fee_growth_outside_b: 5750,
reward_growths_outside: [500, 750, 900],
..Default::default()
},
}] {
// System under test
let update = next_tick_cross_update(
&test.tick,
test.fee_growth_global_a,
test.fee_growth_global_b,
&test.reward_infos,
)
.unwrap();
assert_eq!(
update.fee_growth_outside_a, test.expected_update.fee_growth_outside_a,
"{}: fee_growth_outside_a invalid",
test.name
);
assert_eq!(
update.fee_growth_outside_b, test.expected_update.fee_growth_outside_b,
"{}: fee_growth_outside_b invalid",
test.name
);
let reward_growths_outside = update.reward_growths_outside;
let expected_growths_outside = test.expected_update.reward_growths_outside;
for i in 0..NUM_REWARDS {
assert_eq!(
reward_growths_outside[i], expected_growths_outside[i],
"{}: reward_growth[{}] invalid",
test.name, i
);
}
}
}
}

View File

@ -0,0 +1,219 @@
use crate::errors::ErrorCode;
use crate::math::{add_liquidity_delta, checked_mul_div};
use crate::state::*;
// Calculates the next global reward growth variables based on the given timestamp.
// The provided timestamp must be greater than or equal to the last updated timestamp.
pub fn next_whirlpool_reward_infos(
whirlpool: &Whirlpool,
next_timestamp: u64,
) -> Result<[WhirlpoolRewardInfo; NUM_REWARDS], ErrorCode> {
let curr_timestamp = whirlpool.reward_last_updated_timestamp;
if next_timestamp < curr_timestamp {
return Err(ErrorCode::InvalidTimestamp.into());
}
// No-op if no liquidity or no change in timestamp
if whirlpool.liquidity == 0 || next_timestamp == curr_timestamp {
return Ok(whirlpool.reward_infos);
}
// Calculate new global reward growth
let mut next_reward_infos = whirlpool.reward_infos;
let time_delta = u128::from(next_timestamp - curr_timestamp);
for i in 0..NUM_REWARDS {
if !next_reward_infos[i].initialized() {
continue;
}
let reward_info = &mut next_reward_infos[i];
// Calculate the new reward growth delta.
// If the calculation overflows, set the delta value to zero.
// This will halt reward distributions for this reward.
let reward_growth_delta = checked_mul_div(
time_delta,
reward_info.emissions_per_second_x64,
whirlpool.liquidity,
)
.unwrap_or(0);
// Add the reward growth delta to the global reward growth.
let curr_growth_global = reward_info.growth_global_x64;
reward_info.growth_global_x64 = curr_growth_global.wrapping_add(reward_growth_delta);
}
Ok(next_reward_infos)
}
// Calculates the next global liquidity for a whirlpool depending on its position relative
// to the lower and upper tick indexes and the liquidity_delta.
pub fn next_whirlpool_liquidity(
whirlpool: &Whirlpool,
tick_upper_index: i32,
tick_lower_index: i32,
liquidity_delta: i128,
) -> Result<u128, ErrorCode> {
if whirlpool.tick_current_index < tick_upper_index
&& whirlpool.tick_current_index >= tick_lower_index
{
add_liquidity_delta(whirlpool.liquidity, liquidity_delta)
} else {
Ok(whirlpool.liquidity)
}
}
#[cfg(test)]
mod whirlpool_manager_tests {
use anchor_lang::prelude::Pubkey;
use crate::manager::whirlpool_manager::next_whirlpool_reward_infos;
use crate::math::Q64_RESOLUTION;
use crate::state::whirlpool::WhirlpoolRewardInfo;
use crate::state::whirlpool::NUM_REWARDS;
use crate::state::whirlpool_builder::WhirlpoolBuilder;
use crate::state::Whirlpool;
// Initializes a whirlpool for testing with all the rewards initialized
fn init_test_whirlpool(liquidity: u128, reward_last_updated_timestamp: u64) -> Whirlpool {
WhirlpoolBuilder::new()
.liquidity(liquidity)
.reward_last_updated_timestamp(reward_last_updated_timestamp) // Jan 1 2021 EST
.reward_infos([
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 10 << Q64_RESOLUTION,
growth_global_x64: 100 << Q64_RESOLUTION,
..Default::default()
},
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 0b11 << (Q64_RESOLUTION - 1), // 1.5
growth_global_x64: 200 << Q64_RESOLUTION,
..Default::default()
},
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1 << (Q64_RESOLUTION - 1), // 0.5
growth_global_x64: 300 << Q64_RESOLUTION,
..Default::default()
},
])
.build()
}
#[test]
fn test_next_whirlpool_reward_infos_zero_liquidity_no_op() {
let whirlpool = init_test_whirlpool(0, 1577854800);
let result = next_whirlpool_reward_infos(&whirlpool, 1577855800);
assert_eq!(
WhirlpoolRewardInfo::to_reward_growths(&result.unwrap()),
[
100 << Q64_RESOLUTION,
200 << Q64_RESOLUTION,
300 << Q64_RESOLUTION
]
);
}
#[test]
fn test_next_whirlpool_reward_infos_same_timestamp_no_op() {
let whirlpool = init_test_whirlpool(100, 1577854800);
let result = next_whirlpool_reward_infos(&whirlpool, 1577854800);
assert_eq!(
WhirlpoolRewardInfo::to_reward_growths(&result.unwrap()),
[
100 << Q64_RESOLUTION,
200 << Q64_RESOLUTION,
300 << Q64_RESOLUTION
]
);
}
#[test]
#[should_panic(expected = "InvalidTimestamp")]
fn test_next_whirlpool_reward_infos_invalid_timestamp() {
let whirlpool = &WhirlpoolBuilder::new()
.liquidity(100)
.reward_last_updated_timestamp(1577854800) // Jan 1 2020 EST
.build();
// New timestamp is earlier than the last updated timestamp
next_whirlpool_reward_infos(whirlpool, 1577768400).unwrap(); // Dec 31 2019 EST
}
#[test]
fn test_next_whirlpool_reward_infos_no_initialized_rewards() {
let whirlpool = &WhirlpoolBuilder::new()
.liquidity(100)
.reward_last_updated_timestamp(1577854800) // Jan 1 2021 EST
.build();
let new_timestamp = 1577854800 + 300;
let result = next_whirlpool_reward_infos(whirlpool, new_timestamp).unwrap();
assert_eq!(WhirlpoolRewardInfo::to_reward_growths(&result), [0, 0, 0]);
}
#[test]
fn test_next_whirlpool_reward_infos_some_initialized_rewards() {
let whirlpool = &WhirlpoolBuilder::new()
.liquidity(100)
.reward_last_updated_timestamp(1577854800) // Jan 1 2021 EST
.reward_info(
0,
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: 1 << Q64_RESOLUTION,
..Default::default()
},
)
.build();
let new_timestamp = 1577854800 + 300;
let result = next_whirlpool_reward_infos(whirlpool, new_timestamp).unwrap();
assert_eq!(result[0].growth_global_x64, 3 << Q64_RESOLUTION);
for i in 1..NUM_REWARDS {
assert_eq!(whirlpool.reward_infos[i].growth_global_x64, 0);
}
}
#[test]
fn test_next_whirlpool_reward_infos_delta_zero_on_overflow() {
let whirlpool = &WhirlpoolBuilder::new()
.liquidity(100)
.reward_last_updated_timestamp(0)
.reward_info(
0,
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64: u128::MAX,
growth_global_x64: 100,
..Default::default()
},
)
.build();
let new_timestamp = i64::MAX as u64;
let result = next_whirlpool_reward_infos(whirlpool, new_timestamp).unwrap();
assert_eq!(result[0].growth_global_x64, 100);
}
#[test]
fn test_next_whirlpool_reward_infos_all_initialized_rewards() {
let whirlpool = init_test_whirlpool(100, 1577854800);
let new_timestamp = 1577854800 + 300;
let result = next_whirlpool_reward_infos(&whirlpool, new_timestamp).unwrap();
assert_eq!(result[0].growth_global_x64, 130 << Q64_RESOLUTION);
assert_eq!(
result[1].growth_global_x64,
0b110011001 << (Q64_RESOLUTION - 1) // 204.5
);
assert_eq!(
result[2].growth_global_x64,
0b1001011011 << (Q64_RESOLUTION - 1) // 301.5
);
}
}

View File

@ -0,0 +1,343 @@
use crate::errors::ErrorCode;
use super::U256Muldiv;
pub const Q64_RESOLUTION: u8 = 64;
pub const TO_Q64: u128 = 1u128 << Q64_RESOLUTION;
pub fn checked_mul_div(n0: u128, n1: u128, d: u128) -> Result<u128, ErrorCode> {
checked_mul_div_round_up_if(n0, n1, d, false)
}
pub fn checked_mul_div_round_up(n0: u128, n1: u128, d: u128) -> Result<u128, ErrorCode> {
checked_mul_div_round_up_if(n0, n1, d, true)
}
pub fn checked_mul_div_round_up_if(
n0: u128,
n1: u128,
d: u128,
round_up: bool,
) -> Result<u128, ErrorCode> {
if d == 0 {
return Err(ErrorCode::DivideByZero);
}
let p = n0.checked_mul(n1).ok_or(ErrorCode::MulDivOverflow)?;
let n = p / d;
Ok(if round_up && p % d > 0 { n + 1 } else { n })
}
pub fn checked_mul_shift_right(n0: u128, n1: u128) -> Result<u64, ErrorCode> {
checked_mul_shift_right_round_up_if(n0, n1, false)
}
const Q64_MASK: u128 = 0xFFFF_FFFF_FFFF_FFFF;
/// Multiplies an integer u128 and a Q64.64 fixed point number.
/// Returns a product represented as a u64 integer.
pub fn checked_mul_shift_right_round_up_if(
n0: u128,
n1: u128,
round_up: bool,
) -> Result<u64, ErrorCode> {
if n0 == 0 || n1 == 0 {
return Ok(0);
}
let p = n0
.checked_mul(n1)
.ok_or(ErrorCode::MultiplicationShiftRightOverflow)?;
let result = (p >> Q64_RESOLUTION) as u64;
let should_round = round_up && (p & Q64_MASK > 0);
if should_round && result == u64::MAX {
return Err(ErrorCode::MultiplicationOverflow);
}
Ok(if should_round {
result + 1
} else {
result
})
}
pub fn div_round_up(n: u128, d: u128) -> Result<u128, ErrorCode> {
div_round_up_if(n, d, true)
}
pub fn div_round_up_if(n: u128, d: u128, round_up: bool) -> Result<u128, ErrorCode> {
if d == 0 {
return Err(ErrorCode::DivideByZero);
}
let q = n / d;
Ok(if round_up && n % d > 0 { q + 1 } else { q })
}
pub fn div_round_up_if_u256(
n: U256Muldiv,
d: U256Muldiv,
round_up: bool,
) -> Result<u128, ErrorCode> {
let (quotient, remainder) = n.div(d, round_up);
let result = if round_up && !remainder.is_zero() {
quotient.add(U256Muldiv::new(0, 1))
} else {
quotient
};
Ok(result.try_into_u128()?)
}
#[cfg(test)]
mod fuzz_tests {
use crate::math::U256;
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_div_round_up_if(
n in 0..u128::MAX,
d in 0..u128::MAX,
) {
let rounded = div_round_up(n, d);
if d == 0 {
assert!(rounded.is_err());
} else {
let unrounded = n / d;
let div_unrounded = div_round_up_if(n, d, false)?;
let diff = rounded? - unrounded;
assert!(unrounded == div_unrounded);
assert!(diff <= 1);
assert!((diff == 1) == (n % d > 0));
}
}
#[test]
fn test_div_round_up_if_u256(
n_hi in 0..u128::MAX,
n_lo in 0..u128::MAX,
d_hi in 0..u128::MAX,
d_lo in 0..u128::MAX,
) {
let dividend = U256Muldiv::new(n_hi, n_lo);
let divisor = U256Muldiv::new(d_hi, d_lo);
let rounded = div_round_up_if_u256(dividend, divisor, true);
let (quotient, _) = dividend.div(divisor, true);
if quotient.try_into_u128().is_err() {
assert!(rounded.is_err());
} else {
let other_dividend = (U256::from(n_hi) << 128) + U256::from(n_lo);
let other_divisor = (U256::from(d_hi) << 128) + U256::from(d_lo);
let other_quotient = other_dividend / other_divisor;
let other_remainder = other_dividend % other_divisor;
let unrounded = div_round_up_if_u256(dividend, divisor, false);
assert!(unrounded? == other_quotient.try_into_u128()?);
let diff = rounded.unwrap() - unrounded.unwrap();
assert!(diff <= 1);
assert!((diff == 1) == (other_remainder > U256::zero()));
}
}
#[test]
fn test_checked_mul_div_round_up_if(n0 in 0..u128::MAX, n1 in 0..u128::MAX, d in 0..u128::MAX) {
let result = checked_mul_div_round_up_if(n0, n1, d, true);
if d == 0 {
assert!(result.is_err());
} else if n0.checked_mul(n1).is_none() {
assert!(result.is_err());
} else {
let other_n0 = U256::from(n0);
let other_n1 = U256::from(n1);
let other_p = other_n0 * other_n1;
let other_d = U256::from(d);
let other_result = other_p / other_d;
let unrounded = checked_mul_div_round_up_if(n0, n1, d, false)?;
assert!(U256::from(unrounded) == other_result);
let diff = U256::from(result.unwrap()) - other_result;
assert!(diff <= U256::from(1));
assert!((diff == U256::from(1)) == (other_p % other_d > U256::from(0)));
}
}
#[test]
fn test_mul_shift_right_round_up_if(n0 in 0..u128::MAX, n1 in 0..u128::MAX) {
let result = checked_mul_shift_right_round_up_if(n0, n1, true);
if n0.checked_mul(n1).is_none() {
assert!(result.is_err());
} else {
let p = (U256::from(n0) * U256::from(n1)).try_into_u128()?;
let i = (p >> 64) as u64;
assert!(i == checked_mul_shift_right_round_up_if(n0, n1, false)?);
if i == u64::MAX && (p & Q64_MASK > 0) {
assert!(result.is_err());
} else {
let diff = result.unwrap() - i;
assert!(diff <= 1);
assert!((diff == 1) == (p % (u64::MAX as u128) > 0));
}
}
}
}
}
#[cfg(test)]
mod test_bit_math {
// We arbitrarily select integers a, b, d < 2^128 - 1, such that 2^128 - 1 < (a * b / d) < 2^128
// For simplicity we fix d = 2 and the target to be 2^128 - 0.5
// We then solve for a * b = 2^129 - 1
const MAX_FLOOR: (u128, u128, u128) = (11053036065049294753459639, 61572651155449, 2);
mod test_mul_div {
use crate::math::checked_mul_div;
use super::MAX_FLOOR;
#[test]
fn test_mul_div_ok() {
assert_eq!(checked_mul_div(150, 30, 3).unwrap(), 1500);
assert_eq!(checked_mul_div(15, 0, 10).unwrap(), 0);
}
#[test]
fn test_mul_div_shift_ok() {
assert_eq!(checked_mul_div(u128::MAX, 1, 2).unwrap(), u128::MAX >> 1);
assert_eq!(checked_mul_div(u128::MAX, 1, 4).unwrap(), u128::MAX >> 2);
assert_eq!(checked_mul_div(u128::MAX, 1, 8).unwrap(), u128::MAX >> 3);
assert_eq!(checked_mul_div(u128::MAX, 1, 16).unwrap(), u128::MAX >> 4);
assert_eq!(checked_mul_div(u128::MAX, 1, 32).unwrap(), u128::MAX >> 5);
assert_eq!(checked_mul_div(u128::MAX, 1, 64).unwrap(), u128::MAX >> 6);
}
#[test]
fn test_mul_div_large_ok() {
assert_eq!(
checked_mul_div(u128::MAX, 1, u128::from(u64::MAX) + 1).unwrap(),
u64::MAX.into()
);
assert_eq!(checked_mul_div(u128::MAX - 1, 1, u128::MAX).unwrap(), 0);
}
#[test]
fn test_mul_div_overflows() {
assert!(checked_mul_div(u128::MAX, 2, u128::MAX).is_err());
assert!(checked_mul_div(u128::MAX, u128::MAX, u128::MAX).is_err());
assert!(checked_mul_div(u128::MAX, u128::MAX - 1, u128::MAX).is_err());
assert!(checked_mul_div(u128::MAX, 2, 1).is_err());
assert!(checked_mul_div(MAX_FLOOR.0, MAX_FLOOR.1, MAX_FLOOR.2).is_err());
}
#[test]
fn test_mul_div_does_not_round() {
assert_eq!(checked_mul_div(3, 7, 10).unwrap(), 2);
assert_eq!(
checked_mul_div(u128::MAX, 1, 7).unwrap(),
48611766702991209066196372490252601636
);
}
}
mod test_mul_div_round_up {
use crate::math::checked_mul_div_round_up;
use super::MAX_FLOOR;
#[test]
fn test_mul_div_ok() {
assert_eq!(checked_mul_div_round_up(0, 4, 4).unwrap(), 0);
assert_eq!(checked_mul_div_round_up(2, 4, 4).unwrap(), 2);
assert_eq!(checked_mul_div_round_up(3, 7, 21).unwrap(), 1);
}
#[test]
fn test_mul_div_rounding_up_rounds_up() {
assert_eq!(checked_mul_div_round_up(3, 7, 10).unwrap(), 3);
assert_eq!(
checked_mul_div_round_up(u128::MAX, 1, 7).unwrap(),
48611766702991209066196372490252601637
);
assert_eq!(
checked_mul_div_round_up(u128::MAX - 1, 1, u128::MAX).unwrap(),
1
);
}
#[test]
#[should_panic]
fn test_mul_div_rounding_upfloor_max_panics() {
assert_eq!(
checked_mul_div_round_up(MAX_FLOOR.0, MAX_FLOOR.1, MAX_FLOOR.2).unwrap(),
u128::MAX
);
}
#[test]
fn test_mul_div_overflow_panics() {
assert!(checked_mul_div_round_up(u128::MAX, u128::MAX, 1u128).is_err());
}
}
mod test_div_round_up {
use crate::math::div_round_up;
#[test]
fn test_mul_div_ok() {
assert_eq!(div_round_up(0, 21).unwrap(), 0);
assert_eq!(div_round_up(21, 21).unwrap(), 1);
assert_eq!(div_round_up(8, 4).unwrap(), 2);
}
#[test]
fn test_mul_div_rounding_up_rounds_up() {
assert_eq!(div_round_up(21, 10).unwrap(), 3);
assert_eq!(
div_round_up(u128::MAX, 7).unwrap(),
48611766702991209066196372490252601637
);
assert_eq!(div_round_up(u128::MAX - 1, u128::MAX).unwrap(), 1);
}
}
mod test_mult_shift_right_round_up {
use crate::math::checked_mul_shift_right_round_up_if;
#[test]
fn test_mul_shift_right_ok() {
assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, false).unwrap(), 0);
assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, true).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, false).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, true).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, false).unwrap(), 0);
assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, true).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128 + 1, u32::MAX as u128 + 2, false).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128 + 1, u32::MAX as u128 + 2, true).unwrap(), 2);
}
#[test]
fn test_mul_shift_right_u64_max() {
assert!(checked_mul_shift_right_round_up_if(u128::MAX, 1, true).is_err());
assert_eq!(checked_mul_shift_right_round_up_if(u128::MAX, 1, false).unwrap(), u64::MAX);
}
}
}

View File

@ -0,0 +1,168 @@
#![allow(clippy::assign_op_pattern)]
#![allow(clippy::ptr_offset_with_cast)]
#![allow(clippy::manual_range_contains)]
/// The following code is referenced from drift-labs:
/// https://github.com/drift-labs/protocol-v1/blob/3da78f1f03b66a273fc50818323ac62874abd1d8/programs/clearing_house/src/math/bn.rs
///
/// Based on parity's uint crate
/// https://github.com/paritytech/parity-common/tree/master/uint
///
/// Note: We cannot use U256 from primitive-types (default u256 from parity's uint) because we need to extend the U256 struct to
/// support the Borsh serial/deserialize traits.
///
/// The reason why this custom U256 impl does not directly impl TryInto traits is because of this:
/// https://stackoverflow.com/questions/37347311/how-is-there-a-conflicting-implementation-of-from-when-using-a-generic-type
///
/// As a result, we have to define our own custom Into methods
///
/// U256 reference:
/// https://crates.parity.io/sp_core/struct.U256.html
///
use borsh::{BorshDeserialize, BorshSerialize};
use std::borrow::BorrowMut;
use std::convert::TryInto;
use std::io::{Error, ErrorKind, Write};
use std::mem::size_of;
use uint::construct_uint;
use crate::errors::ErrorCode;
macro_rules! impl_borsh_serialize_for_bn {
($type: ident) => {
impl BorshSerialize for $type {
#[inline]
fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
let bytes = self.to_le_bytes();
writer.write_all(&bytes)
}
}
};
}
macro_rules! impl_borsh_deserialize_for_bn {
($type: ident) => {
impl BorshDeserialize for $type {
#[inline]
fn deserialize(buf: &mut &[u8]) -> std::io::Result<Self> {
if buf.len() < size_of::<$type>() {
return Err(Error::new(
ErrorKind::InvalidInput,
"Unexpected length of input",
));
}
let res = $type::from_le_bytes(buf[..size_of::<$type>()].try_into().unwrap());
*buf = &buf[size_of::<$type>()..];
Ok(res)
}
}
};
}
construct_uint! {
// U256 of [u64; 4]
pub struct U256(4);
}
impl U256 {
pub fn try_into_u64(self) -> Result<u64, ErrorCode> {
self.try_into().map_err(|_| ErrorCode::NumberCastError)
}
pub fn try_into_u128(self) -> Result<u128, ErrorCode> {
self.try_into().map_err(|_| ErrorCode::NumberCastError)
}
pub fn from_le_bytes(bytes: [u8; 32]) -> Self {
U256::from_little_endian(&bytes)
}
pub fn to_le_bytes(self) -> [u8; 32] {
let mut buf: Vec<u8> = Vec::with_capacity(size_of::<Self>());
self.to_little_endian(buf.borrow_mut());
let mut bytes: [u8; 32] = [0u8; 32];
bytes.copy_from_slice(buf.as_slice());
bytes
}
}
impl_borsh_deserialize_for_bn!(U256);
impl_borsh_serialize_for_bn!(U256);
#[cfg(test)]
mod test_u256 {
use super::*;
#[test]
fn test_into_u128_ok() {
let a = U256::from(2653u128);
let b = U256::from(1232u128);
let sum = a + b;
let d: u128 = sum.try_into_u128().unwrap();
assert_eq!(d, 3885u128);
}
#[test]
fn test_into_u128_error() {
let a = U256::from(u128::MAX);
let b = U256::from(u128::MAX);
let sum = a + b;
let c: Result<u128, ErrorCode> = sum.try_into_u128();
assert_eq!(c.is_err(), true);
}
#[test]
fn test_as_u128_ok() {
let a = U256::from(2653u128);
let b = U256::from(1232u128);
let sum = a + b;
let d: u128 = sum.as_u128();
assert_eq!(d, 3885u128);
}
#[test]
#[should_panic(expected = "Integer overflow when casting to u128")]
fn test_as_u128_panic() {
let a = U256::from(u128::MAX);
let b = U256::from(u128::MAX);
let sum = a + b;
let _: u128 = sum.as_u128();
}
#[test]
fn test_into_u64_ok() {
let a = U256::from(2653u64);
let b = U256::from(1232u64);
let sum = a + b;
let d: u64 = sum.try_into_u64().unwrap();
assert_eq!(d, 3885u64);
}
#[test]
fn test_into_u64_error() {
let a = U256::from(u64::MAX);
let b = U256::from(u64::MAX);
let sum = a + b;
let c: Result<u64, ErrorCode> = sum.try_into_u64();
assert_eq!(c.is_err(), true);
}
#[test]
fn test_as_u64_ok() {
let a = U256::from(2653u64);
let b = U256::from(1232u64);
let sum = a + b;
let d: u64 = sum.as_u64();
assert_eq!(d, 3885u64);
}
#[test]
#[should_panic(expected = "Integer overflow when casting to u64")]
fn test_as_u64_panic() {
let a = U256::from(u64::MAX);
let b = U256::from(u64::MAX);
let sum = a + b;
let _: u64 = sum.as_u64(); // panic overflow
}
}

View File

@ -0,0 +1,61 @@
use crate::errors::ErrorCode;
// Adds a signed liquidity delta to a given integer liquidity amount.
// Errors on overflow or underflow.
pub fn add_liquidity_delta(liquidity: u128, delta: i128) -> Result<u128, ErrorCode> {
if delta == 0 {
return Ok(liquidity);
}
if delta > 0 {
liquidity
.checked_add(delta as u128)
.ok_or(ErrorCode::LiquidityOverflow)
} else {
liquidity
.checked_sub(delta.abs() as u128)
.ok_or(ErrorCode::LiquidityUnderflow)
}
}
// Converts an unsigned liquidity amount to a signed liquidity delta
pub fn convert_to_liquidity_delta(
liquidity_amount: u128,
positive: bool,
) -> Result<i128, ErrorCode> {
if liquidity_amount > i128::MAX as u128 {
// The liquidity_amount is converted to a liquidity_delta that is represented as an i128
// By doing this conversion we lose the most significant bit in the u128
// Here we enforce a max value of i128::MAX on the u128 to prevent loss of data.
return Err(ErrorCode::LiquidityTooHigh.into());
}
Ok(if positive {
liquidity_amount as i128
} else {
-(liquidity_amount as i128)
})
}
#[cfg(test)]
mod liquidity_math_tests {
use super::add_liquidity_delta;
use super::ErrorCode;
#[test]
fn test_valid_add_liquidity_delta() {
assert_eq!(add_liquidity_delta(100, 100).unwrap(), 200);
assert_eq!(add_liquidity_delta(100, 0).unwrap(), 100);
assert_eq!(add_liquidity_delta(100, -100).unwrap(), 0);
}
#[test]
fn test_invalid_add_liquidity_delta_overflow() {
let result = add_liquidity_delta(u128::MAX, 1);
assert_eq!(result.unwrap_err(), ErrorCode::LiquidityOverflow);
}
#[test]
fn test_invalid_add_liquidity_delta_underflow() {
let result = add_liquidity_delta(u128::MIN, -1);
assert_eq!(result.unwrap_err(), ErrorCode::LiquidityUnderflow);
}
}

View File

@ -0,0 +1,15 @@
pub mod bit_math;
pub mod bn;
pub mod liquidity_math;
pub mod swap_math;
pub mod tick_math;
pub mod token_math;
pub mod u256_math;
pub use bit_math::*;
pub use bn::*;
pub use liquidity_math::*;
pub use swap_math::*;
pub use tick_math::*;
pub use token_math::*;
pub use u256_math::*;

View File

@ -0,0 +1,942 @@
use std::convert::TryInto;
use crate::errors::ErrorCode;
use crate::math::*;
#[derive(PartialEq, Debug)]
pub struct SwapStepComputation {
pub amount_in: u64,
pub amount_out: u64,
pub next_price: u128,
pub fee_amount: u64,
}
pub fn compute_swap(
amount_remaining: u64,
fee_rate: u16,
liquidity: u128,
sqrt_price_current: u128,
sqrt_price_target: u128,
amount_specified_is_input: bool,
a_to_b: bool,
) -> Result<SwapStepComputation, ErrorCode> {
let fee_amount;
let mut amount_fixed_delta = get_amount_fixed_delta(
sqrt_price_current,
sqrt_price_target,
liquidity,
amount_specified_is_input,
a_to_b,
)?;
let mut amount_calc = amount_remaining;
if amount_specified_is_input {
amount_calc = checked_mul_div(
amount_remaining as u128,
FEE_RATE_MUL_VALUE - fee_rate as u128,
FEE_RATE_MUL_VALUE,
)?
.try_into()?;
}
let next_sqrt_price = if amount_calc >= amount_fixed_delta {
sqrt_price_target
} else {
get_next_sqrt_price(
sqrt_price_current,
liquidity,
amount_calc,
amount_specified_is_input,
a_to_b,
)?
};
let is_max_swap = next_sqrt_price == sqrt_price_target;
let amount_unfixed_delta = get_amount_unfixed_delta(
sqrt_price_current,
next_sqrt_price,
liquidity,
amount_specified_is_input,
a_to_b,
)?;
// If the swap is not at the max, we need to readjust the amount of the fixed token we are using
if !is_max_swap {
amount_fixed_delta = get_amount_fixed_delta(
sqrt_price_current,
next_sqrt_price,
liquidity,
amount_specified_is_input,
a_to_b,
)?;
}
let (amount_in, mut amount_out) = if amount_specified_is_input {
(amount_fixed_delta, amount_unfixed_delta)
} else {
(amount_unfixed_delta, amount_fixed_delta)
};
// Cap output amount if using output
if !amount_specified_is_input && amount_out > amount_remaining {
amount_out = amount_remaining;
}
if amount_specified_is_input && !is_max_swap {
fee_amount = amount_remaining - amount_in;
} else {
fee_amount = checked_mul_div_round_up(
amount_in as u128,
fee_rate as u128,
FEE_RATE_MUL_VALUE - fee_rate as u128,
)?
.try_into()?;
}
Ok(SwapStepComputation {
amount_in,
amount_out,
next_price: next_sqrt_price,
fee_amount,
})
}
fn get_amount_fixed_delta(
sqrt_price_current: u128,
sqrt_price_target: u128,
liquidity: u128,
amount_specified_is_input: bool,
a_to_b: bool,
) -> Result<u64, ErrorCode> {
if a_to_b == amount_specified_is_input {
get_amount_delta_a(
sqrt_price_current,
sqrt_price_target,
liquidity,
amount_specified_is_input,
)
} else {
get_amount_delta_b(
sqrt_price_current,
sqrt_price_target,
liquidity,
amount_specified_is_input,
)
}
}
fn get_amount_unfixed_delta(
sqrt_price_current: u128,
sqrt_price_target: u128,
liquidity: u128,
amount_specified_is_input: bool,
a_to_b: bool,
) -> Result<u64, ErrorCode> {
if a_to_b == amount_specified_is_input {
get_amount_delta_b(
sqrt_price_current,
sqrt_price_target,
liquidity,
!amount_specified_is_input,
)
} else {
get_amount_delta_a(
sqrt_price_current,
sqrt_price_target,
liquidity,
!amount_specified_is_input,
)
}
}
#[cfg(test)]
mod fuzz_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_compute_swap(
amount in 1..u64::MAX,
liquidity in 1..u32::MAX as u128,
fee_rate in 1..u16::MAX,
price_0 in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
price_1 in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
amount_specified_is_input in proptest::bool::ANY,
) {
prop_assume!(price_0 != price_1);
// Rather than use logic to correctly input the prices, we just use the distribution to determine direction
let a_to_b = price_0 >= price_1;
let swap_computation = compute_swap(
amount,
fee_rate,
liquidity,
price_0,
price_1,
amount_specified_is_input,
a_to_b,
).ok().unwrap();
let amount_in = swap_computation.amount_in;
let amount_out = swap_computation.amount_out;
let next_price = swap_computation.next_price;
let fee_amount = swap_computation.fee_amount;
// Amount_in can not exceed maximum amount
assert!(amount_in <= u64::MAX - fee_amount);
// Amounts calculated are less than amount specified
let amount_used = if amount_specified_is_input {
amount_in + fee_amount
} else {
amount_out
};
if next_price != price_1 {
assert!(amount_used == amount);
} else {
assert!(amount_used <= amount);
}
let (price_lower, price_upper) = increasing_price_order(price_0, price_1);
assert!(next_price >= price_lower);
assert!(next_price <= price_upper);
}
#[test]
fn test_compute_swap_inversion(
amount in 1..u64::MAX,
liquidity in 1..u32::MAX as u128,
fee_rate in 1..u16::MAX,
price_0 in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
price_1 in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
amount_specified_is_input in proptest::bool::ANY,
) {
prop_assume!(price_0 != price_1);
// Rather than use logic to correctly input the prices, we just use the distribution to determine direction
let a_to_b = price_0 >= price_1;
let swap_computation = compute_swap(
amount,
fee_rate,
liquidity,
price_0,
price_1,
amount_specified_is_input,
a_to_b,
).ok().unwrap();
let amount_in = swap_computation.amount_in;
let amount_out = swap_computation.amount_out;
let next_price = swap_computation.next_price;
let fee_amount = swap_computation.fee_amount;
let inverted_amount = if amount_specified_is_input {
amount_out
} else {
amount_in + fee_amount
};
if inverted_amount != 0 {
let inverted = compute_swap(
inverted_amount,
fee_rate,
liquidity,
price_0,
price_1,
!amount_specified_is_input,
a_to_b,
).ok().unwrap();
// A to B = price decreasing
// Case 1
// Normal: is_input, a_to_b
// Input is fixed, consume all input to produce amount_out
// amount_in = fixed, ceil
// amount_out = unfixed, floor
// Inverted: !is_input, a_to_b
// amount_in = unfixed, ceil
// amount_out = fixed, floor
// Amount = amount_out, inverted.amount_in and fee <= original input and fee, inverted.amount_out ~~ amount_out, inverted.next_price >= original.next_price
// Case 2
// Normal: !is_input, a_to_b
// Find amount required to get amount_out
// amount_in = unfixed, ceil
// amount_out = fixed, floor
// Inverted: is_input, a_to_b
// amount_in = fixed, ceil
// amount_out = unfixed, floor
// Get max amount_out for input, inverted.amount_in + fee ~~ original input and fee, inverted.amount_out >= amount_out, inverted.next_price <= original.next_price
// Price increasing
// Case 3
// Normal: is_input, !a_to_b
// Input is fixed, consume all input to produce amount_out
// amount_in = fixed, ceil
// amount_out = unfixed, floor
// Inverted: !is_input, !a_to_b
// Amount = amount_out, inverted.amount_in and fee <= original input and fee, inverted.amount_out ~~ amount_out, inverted.next_price <= original.next_price
// Case 4
// Normal: !is_input, !a_to_b
// Find amount required to get amount_out
// amount_in = fixed, floor
// amount_out = unfixed, ceil
// Inverted: is_input, !a_to_b
// Get max amount_out for input, inverted.amount_in + fee ~~ original input and fee, inverted.amount_out >= amount_out
// Since inverted.amount_out >= amount_out and amount in is the same, more of token a is being removed, so
// inverted.next_price >= original.next_price
// Next sqrt price goes from round up to round down
// assert!(inverted.next_price + 1 >= next_price);
if inverted.next_price != price_1 {
if amount_specified_is_input {
// If a_to_b, then goes round up => round down,
assert!(inverted.amount_in <= amount_in);
assert!(inverted.fee_amount <= fee_amount);
} else {
assert!(inverted.amount_in >= amount_in);
assert!(inverted.fee_amount >= fee_amount);
}
assert!(inverted.amount_out >= amount_out);
if a_to_b == amount_specified_is_input {
// Next sqrt price goes from round up to round down
assert!(inverted.next_price >= next_price);
} else {
// Next sqrt price goes from round down to round up
assert!(inverted.next_price <= next_price);
}
// Ratio calculations
// let ratio_in = (u128::from(inverted.amount_in) << 64) / u128::from(amount_in);
// let ratio_out = (u128::from(inverted.amount_out) << 64) / u128::from(amount_out);
// println!("RATIO IN/OUT WHEN INVERTED {} \t| {} ", ratio_in, ratio_out);
// if ratio_out > (2 << 64) || ratio_in < (1 << 63) {
// if ratio_out > (2 << 64) {
// println!("OUT > {}", ratio_out / (1 << 64));
// }
// if ratio_in < (1 << 63) {
// println!("IN < 1/{}", (1 << 64) / ratio_in);
// }
// println!("liq {} | fee {} | price_0 {} | price_1 {} | a_to_b {}", liquidity, fee_rate, price_0, price_1, a_to_b);
// println!("Amount {} | is_input {}", amount, amount_specified_is_input);
// println!("Inverted Amount {} | is_input {}", inverted_amount, !amount_specified_is_input);
// println!("{:?}", swap_computation);
// println!("{:?}", inverted);
// }
}
}
}
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
mod test_swap {
// Doesn't cross any additional ticks
mod no_cross {
use super::*;
#[test]
fn swap_a_to_b_input() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_input_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_output() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_output_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_input() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_input_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_output() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_output_partial() {
validate_tick_whirlpool();
}
}
// Crosses single initialized tick
mod single_tick {
use super::*;
#[test]
fn swap_a_to_b_input() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_input_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_output() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_output_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_input() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_input_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_output() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_output_partial() {
validate_tick_whirlpool();
}
}
// Crosses multiple initialized ticks
mod multi_tick {
use super::*;
#[test]
fn swap_a_to_b_input() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_input_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_output() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_output_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_input() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_input_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_output() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_output_partial() {
validate_tick_whirlpool();
}
}
// Crosses a multiple ticks with a zone of 0 liquidity
mod discontiguous_multi_tick {
use super::*;
#[test]
fn swap_a_to_b_input() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_input_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_output() {
validate_tick_whirlpool();
}
#[test]
fn swap_a_to_b_output_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_input() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_input_partial() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_output() {
validate_tick_whirlpool();
}
#[test]
fn swap_b_to_a_output_partial() {
validate_tick_whirlpool();
}
}
mod protocol_rate {
use super::*;
#[test]
fn protocol_rate() {
validate_tick_whirlpool();
}
#[test]
fn protocol_rate_zero() {
validate_tick_whirlpool();
}
}
fn validate_tick_whirlpool() {
// Validate tick values
// Fee, reward growths
//
// Validate whirlpool values
// liquidity, tick, sqrt_price, fee_growth, reward, protocol fee, token amounts
}
}
mod test_compute_swap {
const TWO_PCT: u16 = 20000;
use std::convert::TryInto;
use super::*;
use crate::math::bit_math::Q64_RESOLUTION;
#[test]
fn swap_a_to_b_input() {
// Example calculation
let amount = 100u128;
let init_liq = 1296;
let init_price = 9;
let price_limit = 4;
// Calculate fee given fee percentage
let fee_amount = div_round_up((amount * u128::from(TWO_PCT)).into(), 1_000_000)
.ok()
.unwrap();
// Calculate initial a and b given L and sqrt(P)
let init_b = init_liq * init_price;
let init_a = init_liq / init_price;
// Calculate amount_in given fee_percentage
let amount_in = amount - fee_amount;
// Swapping a to b =>
let new_a = init_a + amount_in;
// Calculate next price
let next_price = div_round_up(init_liq << Q64_RESOLUTION, new_a)
.ok()
.unwrap();
// b - new_b
let amount_out = init_b - div_round_up(init_liq * init_liq, new_a).ok().unwrap();
test_swap(
100,
TWO_PCT, // 2 % fee
init_liq.try_into().unwrap(), // sqrt(ab)
// Current
// b = 1296 * 9 => 11664
// a = 1296 / 9 => 144
init_price << Q64_RESOLUTION, // sqrt (b/a)
// New
// a = 144 + 98 => 242 => 1296 / sqrt(P) = 242 => sqrt(P) = 1296 /242
// next b = 1296 * 1296 / 242 => 6940
price_limit << Q64_RESOLUTION,
true,
true,
SwapStepComputation {
amount_in: amount_in.try_into().unwrap(),
amount_out: amount_out.try_into().unwrap(),
next_price,
fee_amount: fee_amount.try_into().unwrap(),
},
);
}
#[test]
fn swap_a_to_b_input_zero() {
test_swap(
0,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
4 << Q64_RESOLUTION,
true,
false,
SwapStepComputation {
amount_in: 0,
amount_out: 0,
next_price: 9 << Q64_RESOLUTION,
fee_amount: 0,
},
);
}
#[test]
fn swap_a_to_b_input_zero_liq() {
test_swap(
100,
TWO_PCT,
0,
9 << Q64_RESOLUTION,
4 << Q64_RESOLUTION,
true,
false,
SwapStepComputation {
amount_in: 0,
amount_out: 0,
next_price: 4 << Q64_RESOLUTION,
fee_amount: 0,
},
);
}
#[test]
fn swap_a_to_b_input_max() {
test_swap(
1000,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
4 << Q64_RESOLUTION,
true,
true,
SwapStepComputation {
amount_in: 180,
amount_out: 6480,
next_price: 4 << Q64_RESOLUTION,
fee_amount: 4,
},
);
}
#[test]
fn swap_a_to_b_input_max_1pct_fee() {
test_swap(
1000,
TWO_PCT / 2,
1296,
9 << Q64_RESOLUTION,
4 << Q64_RESOLUTION,
true,
true,
SwapStepComputation {
amount_in: 180,
amount_out: 6480,
next_price: 4 << Q64_RESOLUTION,
fee_amount: 2,
},
);
}
#[test]
fn swap_a_to_b_output() {
test_swap(
4723,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
4 << Q64_RESOLUTION,
false,
true,
SwapStepComputation {
amount_in: 98,
amount_out: 4723,
next_price: 98795409425631171116,
fee_amount: 2,
},
);
}
#[test]
fn swap_a_to_b_output_max() {
test_swap(
10000,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
4 << Q64_RESOLUTION,
false,
true,
SwapStepComputation {
amount_in: 180,
amount_out: 6480,
next_price: 4 << Q64_RESOLUTION,
fee_amount: 4,
},
);
}
#[test]
fn swap_a_to_b_output_zero() {
test_swap(
0,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
4 << Q64_RESOLUTION,
false,
true,
SwapStepComputation {
amount_in: 0,
amount_out: 0,
next_price: 9 << Q64_RESOLUTION,
fee_amount: 0,
},
);
}
#[test]
fn swap_a_to_b_output_zero_liq() {
test_swap(
100,
TWO_PCT,
0,
9 << Q64_RESOLUTION,
4 << Q64_RESOLUTION,
false,
true,
SwapStepComputation {
amount_in: 0,
amount_out: 0,
next_price: 4 << Q64_RESOLUTION,
fee_amount: 0,
},
);
}
#[test]
fn swap_b_to_a_input() {
test_swap(
2000,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
16 << Q64_RESOLUTION,
true,
false,
SwapStepComputation {
amount_in: 1960,
amount_out: 20,
next_price: 193918550355107200012,
fee_amount: 40,
},
);
}
#[test]
fn swap_b_to_a_input_max() {
test_swap(
20000,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
16 << Q64_RESOLUTION,
true,
false,
SwapStepComputation {
amount_in: 9072,
amount_out: 63,
next_price: 16 << Q64_RESOLUTION,
fee_amount: 186,
},
);
}
#[test]
fn swap_b_to_a_input_zero() {
test_swap(
0,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
16 << Q64_RESOLUTION,
true,
false,
SwapStepComputation {
amount_in: 0,
amount_out: 0,
next_price: 9 << Q64_RESOLUTION,
fee_amount: 0,
},
);
}
#[test]
fn swap_b_to_a_input_zero_liq() {
test_swap(
100,
TWO_PCT,
0,
9 << Q64_RESOLUTION,
16 << Q64_RESOLUTION,
true,
false,
SwapStepComputation {
amount_in: 0,
amount_out: 0,
next_price: 16 << Q64_RESOLUTION,
fee_amount: 0,
},
);
}
#[test]
fn swap_b_to_a_output() {
test_swap(
20,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
16 << Q64_RESOLUTION,
false,
false,
SwapStepComputation {
amount_in: 1882,
amount_out: 20,
next_price: 192798228383286926568,
fee_amount: 39,
},
);
}
#[test]
fn swap_b_to_a_output_max() {
test_swap(
80,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
16 << Q64_RESOLUTION,
false,
false,
SwapStepComputation {
amount_in: 9072,
amount_out: 63,
next_price: 16 << Q64_RESOLUTION,
fee_amount: 186,
},
);
}
#[test]
fn swap_b_to_a_output_zero() {
test_swap(
0,
TWO_PCT,
1296,
9 << Q64_RESOLUTION,
16 << Q64_RESOLUTION,
false,
false,
SwapStepComputation {
amount_in: 0,
amount_out: 0,
next_price: 9 << Q64_RESOLUTION,
fee_amount: 0,
},
);
}
#[test]
fn swap_b_to_a_output_zero_liq() {
test_swap(
100,
TWO_PCT,
0,
9 << Q64_RESOLUTION,
16 << Q64_RESOLUTION,
false,
false,
SwapStepComputation {
amount_in: 0,
amount_out: 0,
next_price: 16 << Q64_RESOLUTION,
fee_amount: 0,
},
);
}
}
fn test_swap(
amount_remaining: u64,
fee_rate: u16,
liquidity: u128,
sqrt_price_current: u128,
sqrt_price_target_limit: u128,
amount_specified_is_input: bool,
a_to_b: bool,
expected: SwapStepComputation,
) {
let swap_computation = compute_swap(
amount_remaining,
fee_rate,
liquidity,
sqrt_price_current,
sqrt_price_target_limit,
amount_specified_is_input,
a_to_b,
);
assert_eq!(swap_computation.ok().unwrap(), expected);
}
}

View File

@ -0,0 +1,595 @@
use crate::math::u256_math::*;
use std::convert::TryInto;
// Max/Min sqrt_price derived from max/min tick-index
pub const MAX_SQRT_PRICE_X64: u128 = 79226673515401279992447579055;
pub const MIN_SQRT_PRICE_X64: u128 = 4295048016;
const LOG_B_2_X32: i128 = 59543866431248i128;
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
/// 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.
///
/// # Parameters
/// - `tick` - A i32 integer representing the tick integer
///
/// # Returns
/// - `Ok`: A u128 Q32.64 representing the sqrt_price
pub fn sqrt_price_from_tick_index(tick: i32) -> u128 {
if tick >= 0 {
get_sqrt_price_positive_tick(tick)
} else {
get_sqrt_price_negative_tick(tick)
}
}
/// Derive the tick-index from a sqrt-price. The precision of this method is only guarranted
/// if sqrt-price is within the bounds of {max, min} sqrt-price.
///
/// # Parameters
/// - `sqrt_price_x64` - A u128 Q64.64 integer representing the sqrt-price
///
/// # Returns
/// - An i32 representing the tick_index of the provided sqrt-price
pub fn tick_index_from_sqrt_price(sqrt_price_x64: &u128) -> i32 {
// Determine log_b(sqrt_ratio). First by calculating integer portion (msb)
let msb: u32 = 128 - sqrt_price_x64.leading_zeros() - 1;
let log2p_integer_x32 = (msb as i128 - 64) << 32;
// get fractional value (r/2^msb), msb always > 128
// We begin the iteration from bit 63 (0.5 in Q64.64)
let mut bit: i128 = 0x8000_0000_0000_0000i128;
let mut precision = 0;
let mut log2p_fraction_x64 = 0;
// Log2 iterative approximation for the fractional part
// Go through each 2^(j) bit where j < 64 in a Q64.64 number
// Append current bit value to fraction result if r^2 Q2.126 is more than 2
let mut r = if msb >= 64 {
sqrt_price_x64 >> (msb - 63)
} else {
sqrt_price_x64 << (63 - msb)
};
while bit > 0 && precision < BIT_PRECISION {
r *= r;
let is_r_more_than_two = r >> 127 as u32;
r >>= 63 + is_r_more_than_two;
log2p_fraction_x64 += bit * is_r_more_than_two as i128;
bit >>= 1;
precision += 1;
}
let log2p_fraction_x32 = log2p_fraction_x64 >> 32;
let log2p_x32 = log2p_integer_x32 + log2p_fraction_x32;
// Transform from base 2 to base b
let logbp_x64 = log2p_x32 * LOG_B_2_X32;
// Derive tick_low & high estimate. Adjust with the possibility of under-estimating by 2^precision_bits/log_2(b) + 0.01 error margin.
let tick_low: i32 = ((logbp_x64 - LOG_B_P_ERR_MARGIN_LOWER_X64) >> 64)
.try_into()
.unwrap();
let tick_high: i32 = ((logbp_x64 + LOG_B_P_ERR_MARGIN_UPPER_X64) >> 64)
.try_into()
.unwrap();
let result_tick = if tick_low == tick_high {
tick_low
} else {
// If our estimation for tick_high returns a lower sqrt_price than the input
// then the actual tick_high has to be higher than than tick_high.
// Otherwise, the actual value is between tick_low & tick_high, so a floor value
// (tick_low) is returned
let actual_tick_high_sqrt_price_x64: u128 = sqrt_price_from_tick_index(tick_high);
if actual_tick_high_sqrt_price_x64 <= *sqrt_price_x64 {
tick_high
} else {
tick_low
}
};
result_tick
}
fn mul_shift_96(n0: u128, n1: u128) -> u128 {
mul_u256(n0, n1).shift_right(96).try_into_u128().unwrap()
}
// Performs the exponential conversion with Q64.64 precision
fn get_sqrt_price_positive_tick(tick: i32) -> u128 {
let mut ratio: u128 = if tick & 1 != 0 {
79232123823359799118286999567
} else {
79228162514264337593543950336
};
if tick & 2 != 0 {
ratio = mul_shift_96(ratio, 79236085330515764027303304731);
}
if tick & 4 != 0 {
ratio = mul_shift_96(ratio, 79244008939048815603706035061);
}
if tick & 8 != 0 {
ratio = mul_shift_96(ratio, 79259858533276714757314932305);
}
if tick & 16 != 0 {
ratio = mul_shift_96(ratio, 79291567232598584799939703904);
}
if tick & 32 != 0 {
ratio = mul_shift_96(ratio, 79355022692464371645785046466);
}
if tick & 64 != 0 {
ratio = mul_shift_96(ratio, 79482085999252804386437311141);
}
if tick & 128 != 0 {
ratio = mul_shift_96(ratio, 79736823300114093921829183326);
}
if tick & 256 != 0 {
ratio = mul_shift_96(ratio, 80248749790819932309965073892);
}
if tick & 512 != 0 {
ratio = mul_shift_96(ratio, 81282483887344747381513967011);
}
if tick & 1024 != 0 {
ratio = mul_shift_96(ratio, 83390072131320151908154831281);
}
if tick & 2048 != 0 {
ratio = mul_shift_96(ratio, 87770609709833776024991924138);
}
if tick & 4096 != 0 {
ratio = mul_shift_96(ratio, 97234110755111693312479820773);
}
if tick & 8192 != 0 {
ratio = mul_shift_96(ratio, 119332217159966728226237229890);
}
if tick & 16384 != 0 {
ratio = mul_shift_96(ratio, 179736315981702064433883588727);
}
if tick & 32768 != 0 {
ratio = mul_shift_96(ratio, 407748233172238350107850275304);
}
if tick & 65536 != 0 {
ratio = mul_shift_96(ratio, 2098478828474011932436660412517);
}
if tick & 131072 != 0 {
ratio = mul_shift_96(ratio, 55581415166113811149459800483533);
}
if tick & 262144 != 0 {
ratio = mul_shift_96(ratio, 38992368544603139932233054999993551);
}
ratio >> 32
}
fn get_sqrt_price_negative_tick(tick: i32) -> u128 {
let abs_tick = tick.abs();
let mut ratio: u128 = if abs_tick & 1 != 0 {
18445821805675392311
} else {
18446744073709551616
};
if abs_tick & 2 != 0 {
ratio = (ratio * 18444899583751176498) >> 64
}
if abs_tick & 4 != 0 {
ratio = (ratio * 18443055278223354162) >> 64
}
if abs_tick & 8 != 0 {
ratio = (ratio * 18439367220385604838) >> 64
}
if abs_tick & 16 != 0 {
ratio = (ratio * 18431993317065449817) >> 64
}
if abs_tick & 32 != 0 {
ratio = (ratio * 18417254355718160513) >> 64
}
if abs_tick & 64 != 0 {
ratio = (ratio * 18387811781193591352) >> 64
}
if abs_tick & 128 != 0 {
ratio = (ratio * 18329067761203520168) >> 64
}
if abs_tick & 256 != 0 {
ratio = (ratio * 18212142134806087854) >> 64
}
if abs_tick & 512 != 0 {
ratio = (ratio * 17980523815641551639) >> 64
}
if abs_tick & 1024 != 0 {
ratio = (ratio * 17526086738831147013) >> 64
}
if abs_tick & 2048 != 0 {
ratio = (ratio * 16651378430235024244) >> 64
}
if abs_tick & 4096 != 0 {
ratio = (ratio * 15030750278693429944) >> 64
}
if abs_tick & 8192 != 0 {
ratio = (ratio * 12247334978882834399) >> 64
}
if abs_tick & 16384 != 0 {
ratio = (ratio * 8131365268884726200) >> 64
}
if abs_tick & 32768 != 0 {
ratio = (ratio * 3584323654723342297) >> 64
}
if abs_tick & 65536 != 0 {
ratio = (ratio * 696457651847595233) >> 64
}
if abs_tick & 131072 != 0 {
ratio = (ratio * 26294789957452057) >> 64
}
if abs_tick & 262144 != 0 {
ratio = (ratio * 37481735321082) >> 64
}
ratio
}
#[cfg(test)]
mod fuzz_tests {
use super::*;
use crate::{
math::U256,
state::{MAX_TICK_INDEX, MIN_TICK_INDEX},
};
use proptest::prelude::*;
fn within_price_approximation(lower: u128, upper: u128) -> bool {
let precision = 96;
// We increase the resolution of upper to find ratio_x96
let x = U256::from(upper) << precision;
let y = U256::from(lower);
// (1.0001 ^ 0.5) << 96 (precision)
let sqrt_10001_x96 = 79232123823359799118286999567u128;
// This ratio should be as close to sqrt_10001_x96 as possible
let ratio_x96 = x.div_mod(y).0.as_u128();
// Find absolute error in ratio in x96
let error = if sqrt_10001_x96 > ratio_x96 {
sqrt_10001_x96 - ratio_x96
} else {
ratio_x96 - sqrt_10001_x96
};
// Calculate number of error bits
let error_bits = 128 - error.leading_zeros();
return precision - error_bits >= 32;
}
proptest! {
#[test]
fn test_tick_index_to_sqrt_price (
tick in MIN_TICK_INDEX..MAX_TICK_INDEX,
) {
let sqrt_price = sqrt_price_from_tick_index(tick);
// Check bounds
assert!(sqrt_price >= MIN_SQRT_PRICE_X64);
assert!(sqrt_price <= MAX_SQRT_PRICE_X64);
// Check the inverted tick has unique price and within bounds
let minus_tick_price = sqrt_price_from_tick_index(tick - 1);
let plus_tick_price = sqrt_price_from_tick_index(tick + 1);
assert!(minus_tick_price < sqrt_price && sqrt_price < plus_tick_price);
// Check that sqrt_price_from_tick_index(tick + 1) approximates sqrt(1.0001) * sqrt_price_from_tick_index(tick)
assert!(within_price_approximation(minus_tick_price, sqrt_price));
assert!(within_price_approximation(sqrt_price, plus_tick_price));
}
#[test]
fn test_tick_index_from_sqrt_price (
sqrt_price in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64
) {
let tick = tick_index_from_sqrt_price(&sqrt_price);
// Check bounds
assert!(tick >= MIN_TICK_INDEX);
assert!(tick < MAX_TICK_INDEX);
// Check the inverted price from the calculated tick is within tick boundaries
assert!(sqrt_price >= sqrt_price_from_tick_index(tick) && sqrt_price < sqrt_price_from_tick_index(tick + 1))
}
#[test]
// Verify that both conversion functions are symmetrical.
fn test_tick_index_and_sqrt_price_symmetry (
tick in MIN_TICK_INDEX..MAX_TICK_INDEX
) {
let sqrt_price_x64 = sqrt_price_from_tick_index(tick);
let resolved_tick = tick_index_from_sqrt_price(&sqrt_price_x64);
assert!(resolved_tick == tick);
}
#[test]
fn test_sqrt_price_from_tick_index_is_sequence (
tick in MIN_TICK_INDEX-1..MAX_TICK_INDEX
) {
let sqrt_price_x64 = sqrt_price_from_tick_index(tick);
let last_sqrt_price_x64 = sqrt_price_from_tick_index(tick-1);
assert!(last_sqrt_price_x64 < sqrt_price_x64);
}
#[test]
fn test_tick_index_from_sqrt_price_is_sequence (
sqrt_price in (MIN_SQRT_PRICE_X64 + 10)..MAX_SQRT_PRICE_X64
) {
let tick = tick_index_from_sqrt_price(&sqrt_price);
let last_tick = tick_index_from_sqrt_price(&(sqrt_price - 10));
assert!(last_tick <= tick);
}
}
}
#[cfg(test)]
mod test_tick_index_from_sqrt_price {
use super::*;
use crate::state::{MAX_TICK_INDEX, MIN_TICK_INDEX};
#[test]
fn test_sqrt_price_from_tick_index_at_max() {
let r = tick_index_from_sqrt_price(&MAX_SQRT_PRICE_X64);
assert_eq!(&r, &MAX_TICK_INDEX);
}
#[test]
fn test_sqrt_price_from_tick_index_at_min() {
let r = tick_index_from_sqrt_price(&MIN_SQRT_PRICE_X64);
assert_eq!(&r, &MIN_TICK_INDEX);
}
#[test]
fn test_sqrt_price_from_tick_index_at_max_add_one() {
let sqrt_price_x64_max_add_one = MAX_SQRT_PRICE_X64 + 1;
let tick_from_max_add_one = tick_index_from_sqrt_price(&sqrt_price_x64_max_add_one);
let sqrt_price_x64_max = MAX_SQRT_PRICE_X64 + 1;
let tick_from_max = tick_index_from_sqrt_price(&sqrt_price_x64_max);
// We don't care about accuracy over the limit. We just care about it's equality properties.
assert_eq!(tick_from_max_add_one >= tick_from_max, true);
}
#[test]
fn test_sqrt_price_from_tick_index_at_min_add_one() {
let sqrt_price_x64 = MIN_SQRT_PRICE_X64 + 1;
let r = tick_index_from_sqrt_price(&sqrt_price_x64);
assert_eq!(&r, &(MIN_TICK_INDEX));
}
#[test]
fn test_sqrt_price_from_tick_index_at_max_sub_one() {
let sqrt_price_x64 = MAX_SQRT_PRICE_X64 - 1;
let r = tick_index_from_sqrt_price(&sqrt_price_x64);
assert_eq!(&r, &(MAX_TICK_INDEX - 1));
}
#[test]
fn test_sqrt_price_from_tick_index_at_min_sub_one() {
let sqrt_price_x64_min_sub_one = MIN_SQRT_PRICE_X64 - 1;
let tick_from_min_sub_one = tick_index_from_sqrt_price(&sqrt_price_x64_min_sub_one);
let sqrt_price_x64_min = MIN_SQRT_PRICE_X64 + 1;
let tick_from_min = tick_index_from_sqrt_price(&sqrt_price_x64_min);
// We don't care about accuracy over the limit. We just care about it's equality properties.
assert_eq!(tick_from_min_sub_one < tick_from_min, true);
}
#[test]
fn test_sqrt_price_from_tick_index_at_one() {
let sqrt_price_x64: u128 = u64::MAX as u128 + 1;
let r = tick_index_from_sqrt_price(&sqrt_price_x64);
assert_eq!(r, 0);
}
#[test]
fn test_sqrt_price_from_tick_index_at_one_add_one() {
let sqrt_price_x64: u128 = u64::MAX as u128 + 2;
let r = tick_index_from_sqrt_price(&sqrt_price_x64);
assert_eq!(r, 0);
}
#[test]
fn test_sqrt_price_from_tick_index_at_one_sub_one() {
let sqrt_price_x64: u128 = u64::MAX.into();
let r = tick_index_from_sqrt_price(&sqrt_price_x64);
assert_eq!(r, -1);
}
}
#[cfg(test)]
mod sqrt_price_from_tick_index_tests {
use super::*;
use crate::state::{MAX_TICK_INDEX, MIN_TICK_INDEX};
#[test]
#[should_panic(expected = "NumberDownCastError")]
// There should never be a use-case where we call this method with an out of bound index
fn test_tick_exceed_max() {
let sqrt_price_from_max_tick_add_one = sqrt_price_from_tick_index(MAX_TICK_INDEX + 1);
let sqrt_price_from_max_tick = sqrt_price_from_tick_index(MAX_TICK_INDEX);
assert_eq!(
sqrt_price_from_max_tick_add_one > sqrt_price_from_max_tick,
true
);
}
#[test]
fn test_tick_below_min() {
let sqrt_price_from_min_tick_sub_one = sqrt_price_from_tick_index(MIN_TICK_INDEX - 1);
let sqrt_price_from_min_tick = sqrt_price_from_tick_index(MIN_TICK_INDEX);
assert_eq!(
sqrt_price_from_min_tick_sub_one < sqrt_price_from_min_tick,
true
);
}
#[test]
fn test_tick_at_max() {
let max_tick = MAX_TICK_INDEX;
let r = sqrt_price_from_tick_index(max_tick);
assert_eq!(r, MAX_SQRT_PRICE_X64);
}
#[test]
fn test_tick_at_min() {
let min_tick = MIN_TICK_INDEX;
let r = sqrt_price_from_tick_index(min_tick);
assert_eq!(r, MIN_SQRT_PRICE_X64);
}
#[test]
fn test_exact_bit_values() {
let conditions = &[
(
0i32,
18446744073709551616u128,
18446744073709551616u128,
"0x0",
),
(
1i32,
18447666387855959850u128,
18445821805675392311u128,
"0x1",
),
(
2i32,
18448588748116922571u128,
18444899583751176498u128,
"0x2",
),
(
4i32,
18450433606991734263u128,
18443055278223354162u128,
"0x4",
),
(
8i32,
18454123878217468680u128,
18439367220385604838u128,
"0x8",
),
(
16i32,
18461506635090006701u128,
18431993317065449817u128,
"0x10",
),
(
32i32,
18476281010653910144u128,
18417254355718160513u128,
"0x20",
),
(
64i32,
18505865242158250041u128,
18387811781193591352u128,
"0x40",
),
(
128i32,
18565175891880433522u128,
18329067761203520168u128,
"0x80",
),
(
256i32,
18684368066214940582u128,
18212142134806087854u128,
"0x100",
),
(
512i32,
18925053041275764671u128,
17980523815641551639u128,
"0x200",
),
(
1024i32,
19415764168677886926u128,
17526086738831147013u128,
"0x400",
),
(
2048i32,
20435687552633177494u128,
16651378430235024244u128,
"0x800",
),
(
4096i32,
22639080592224303007u128,
15030750278693429944u128,
"0x1000",
),
(
8192i32,
27784196929998399742u128,
12247334978882834399u128,
"0x2000",
),
(
16384i32,
41848122137994986128u128,
8131365268884726200u128,
"0x4000",
),
(
32768i32,
94936283578220370716u128,
3584323654723342297u128,
"0x8000",
),
(
65536i32,
488590176327622479860u128,
696457651847595233u128,
"0x10000",
),
(
131072i32,
12941056668319229769860u128,
26294789957452057u128,
"0x20000",
),
(
262144i32,
9078618265828848800676189u128,
37481735321082u128,
"0x40000",
),
];
for (p_tick, expected, neg_expected, desc) in conditions {
let p_result = sqrt_price_from_tick_index(*p_tick);
let n_tick = -p_tick;
let n_result = sqrt_price_from_tick_index(n_tick);
assert_eq!(
p_result, *expected,
"Assert positive tick equals expected value on binary fraction bit = {} ",
desc
);
assert_eq!(
n_result, *neg_expected,
"Assert negative tick equals expected value on binary fraction bit = {} ",
desc
);
}
}
}

View File

@ -0,0 +1,495 @@
use crate::errors::ErrorCode;
use crate::math::Q64_RESOLUTION;
use super::{
checked_mul_shift_right_round_up_if, div_round_up_if, div_round_up_if_u256, mul_u256,
U256Muldiv, MAX_SQRT_PRICE_X64, MIN_SQRT_PRICE_X64,
};
// Fee rate is represented as hundredths of a basis point.
// Fee amount = total_amount * fee_rate / 1_000_000.
// Max fee rate supported is 1%.
pub const MAX_FEE_RATE: u16 = 10_000;
// Assuming that FEE_RATE is represented as hundredths of a basis point
// We want FEE_RATE_MUL_VALUE = 1/FEE_RATE_UNIT, so 1e6
pub const FEE_RATE_MUL_VALUE: u128 = 1_000_000;
// Protocol fee rate is represented as a basis point.
// Protocol fee amount = fee_amount * protocol_fee_rate / 10_000.
// Max protocol fee rate supported is 25% of the fee rate.
pub const MAX_PROTOCOL_FEE_RATE: u16 = 2_500;
// Assuming that PROTOCOL_FEE_RATE is represented as a basis point
// We want PROTOCOL_FEE_RATE_MUL_VALUE = 1/PROTOCOL_FEE_UNIT, so 1e4
pub const PROTOCOL_FEE_RATE_MUL_VALUE: u128 = 10_000;
//
// Get change in token_a corresponding to a change in price
//
// 6.16
// Δt_a = Δ(1 / sqrt_price) * liquidity
// Replace delta
// Δt_a = (1 / sqrt_price_upper - 1 / sqrt_price_lower) * liquidity
// Common denominator to simplify
// Δt_a = ((sqrt_price_lower - sqrt_price_upper) / (sqrt_price_upper * sqrt_price_lower)) * liquidity
// Δt_a = (liquidity * (sqrt_price_lower - sqrt_price_upper)) / (sqrt_price_upper * sqrt_price_lower)
pub fn get_amount_delta_a(
sqrt_price_0: u128,
sqrt_price_1: u128,
liquidity: u128,
round_up: bool,
) -> Result<u64, ErrorCode> {
let (sqrt_price_lower, sqrt_price_upper) = increasing_price_order(sqrt_price_0, sqrt_price_1);
let sqrt_price_diff = sqrt_price_upper - sqrt_price_lower;
let numerator = mul_u256(liquidity, sqrt_price_diff)
.checked_shift_word_left()
.ok_or(ErrorCode::MultiplicationOverflow)?;
let denominator = mul_u256(sqrt_price_upper, sqrt_price_lower);
let (quotient, remainder) = numerator.div(denominator, round_up);
let result = if round_up && !remainder.is_zero() {
quotient.add(U256Muldiv::new(0, 1)).try_into_u128()?
} else {
quotient.try_into_u128()?
};
if result > u64::MAX as u128 {
return Err(ErrorCode::TokenMaxExceeded);
}
return Ok(result as u64);
}
//
// Get change in token_b corresponding to a change in price
//
// 6.14
// Δt_b = Δ(sqrt_price) * liquidity
// Replace delta
// Δt_b = (sqrt_price_upper - sqrt_price_lower) * liquidity
pub fn get_amount_delta_b(
sqrt_price_0: u128,
sqrt_price_1: u128,
liquidity: u128,
round_up: bool,
) -> Result<u64, ErrorCode> {
let (price_lower, price_upper) = increasing_price_order(sqrt_price_0, sqrt_price_1);
// liquidity * (price_upper - price_lower) must be less than 2^128
// for the token amount to be less than 2^64
checked_mul_shift_right_round_up_if(liquidity, price_upper - price_lower, round_up)
}
pub fn increasing_price_order(sqrt_price_0: u128, sqrt_price_1: u128) -> (u128, u128) {
if sqrt_price_0 > sqrt_price_1 {
(sqrt_price_1, sqrt_price_0)
} else {
(sqrt_price_0, sqrt_price_1)
}
}
//
// Get change in price corresponding to a change in token_a supply
//
// 6.15
// Δ(1 / sqrt_price) = Δt_a / liquidity
//
// Replace delta
// 1 / sqrt_price_new - 1 / sqrt_price = amount / liquidity
//
// Move sqrt price to other side
// 1 / sqrt_price_new = (amount / liquidity) + (1 / sqrt_price)
//
// Common denominator for right side
// 1 / sqrt_price_new = (sqrt_price * amount + liquidity) / (sqrt_price * liquidity)
//
// Invert fractions
// sqrt_price_new = (sqrt_price * liquidity) / (liquidity + amount * sqrt_price)
pub fn get_next_sqrt_price_from_a_round_up(
sqrt_price: u128,
liquidity: u128,
amount: u64,
amount_specified_is_input: bool,
) -> Result<u128, ErrorCode> {
if amount == 0 {
return Ok(sqrt_price);
}
let product = mul_u256(sqrt_price, amount as u128);
let numerator = mul_u256(liquidity, sqrt_price)
.checked_shift_word_left()
.ok_or(ErrorCode::MultiplicationOverflow)?;
// In this scenario the denominator will end up being < 0
let liquidity_shift_left = U256Muldiv::new(0, liquidity).shift_word_left();
if !amount_specified_is_input && liquidity_shift_left.lte(product) {
return Err(ErrorCode::DivideByZero);
}
let denominator = if amount_specified_is_input {
liquidity_shift_left.add(product)
} else {
liquidity_shift_left.sub(product)
};
let price = div_round_up_if_u256(numerator, denominator, true)?;
if price < MIN_SQRT_PRICE_X64 {
return Err(ErrorCode::TokenMinSubceeded);
} else if price > MAX_SQRT_PRICE_X64 {
return Err(ErrorCode::TokenMaxExceeded);
}
Ok(price)
}
//
// Get change in price corresponding to a change in token_b supply
//
// 6.13
// Δ(sqrt_price) = Δt_b / liquidity
pub fn get_next_sqrt_price_from_b_round_down(
sqrt_price: u128,
liquidity: u128,
amount: u64,
amount_specified_is_input: bool,
) -> Result<u128, ErrorCode> {
// We always want square root price to be rounded down, which means
// Case 3. If we are fixing input (adding B), we are increasing price, we want delta to be floor(delta)
// sqrt_price + floor(delta) < sqrt_price + delta
//
// Case 4. If we are fixing output (removing B), we are decreasing price, we want delta to be ceil(delta)
// sqrt_price - ceil(delta) < sqrt_price - delta
// Q64.0 << 64 => Q64.64
let amount_x64 = (amount as u128) << Q64_RESOLUTION;
// Q64.64 / Q64.0 => Q64.64
let delta = div_round_up_if(amount_x64, liquidity, !amount_specified_is_input)?;
// Q64(32).64 +/- Q64.64
if amount_specified_is_input {
// We are adding token b to supply, causing price to increase
sqrt_price
.checked_add(delta)
.ok_or(ErrorCode::SqrtPriceOutOfBounds)
} else {
// We are removing token b from supply,. causing price to decrease
sqrt_price
.checked_sub(delta)
.ok_or(ErrorCode::SqrtPriceOutOfBounds)
}
}
pub fn get_next_sqrt_price(
sqrt_price: u128,
liquidity: u128,
amount: u64,
amount_specified_is_input: bool,
a_to_b: bool,
) -> Result<u128, ErrorCode> {
if amount_specified_is_input == a_to_b {
// We are fixing A
// Case 1. amount_specified_is_input = true, a_to_b = true
// We are exchanging A to B with at most _amount_ of A (input)
//
// Case 2. amount_specified_is_input = false, a_to_b = false
// We are exchanging B to A wanting to guarantee at least _amount_ of A (output)
//
// In either case we want the sqrt_price to be rounded up.
//
// Eq 1. sqrt_price = sqrt( b / a )
//
// Case 1. amount_specified_is_input = true, a_to_b = true
// We are adding token A to the supply, causing price to decrease (Eq 1.)
// Since we are fixing input, we can not exceed the amount that is being provided by the user.
// Because a higher price is inversely correlated with an increased supply of A,
// a higher price means we are adding less A. Thus when performing math, we wish to round the
// price up, since that means that we are guaranteed to not exceed the fixed amount of A provided.
//
// Case 2. amount_specified_is_input = false, a_to_b = false
// We are removing token A from the supply, causing price to increase (Eq 1.)
// Since we are fixing output, we want to guarantee that the user is provided at least _amount_ of A
// Because a higher price is correlated with a decreased supply of A,
// a higher price means we are removing more A to give to the user. Thus when performing math, we wish
// to round the price up, since that means we guarantee that user receives at least _amount_ of A
get_next_sqrt_price_from_a_round_up(
sqrt_price,
liquidity,
amount,
amount_specified_is_input,
)
} else {
// We are fixing B
// Case 3. amount_specified_is_input = true, a_to_b = false
// We are exchanging B to A using at most _amount_ of B (input)
//
// Case 4. amount_specified_is_input = false, a_to_b = true
// We are exchanging A to B wanting to guarantee at least _amount_ of B (output)
//
// In either case we want the sqrt_price to be rounded down.
//
// Eq 1. sqrt_price = sqrt( b / a )
//
// Case 3. amount_specified_is_input = true, a_to_b = false
// We are adding token B to the supply, causing price to increase (Eq 1.)
// Since we are fixing input, we can not exceed the amount that is being provided by the user.
// Because a lower price is inversely correlated with an increased supply of B,
// a lower price means that we are adding less B. Thus when performing math, we wish to round the
// price down, since that means that we are guaranteed to not exceed the fixed amount of B provided.
//
// Case 4. amount_specified_is_input = false, a_to_b = true
// We are removing token B from the supply, causing price to decrease (Eq 1.)
// Since we are fixing output, we want to guarantee that the user is provided at least _amount_ of B
// Because a lower price is correlated with a decreased supply of B,
// a lower price means we are removing more B to give to the user. Thus when performing math, we
// wish to round the price down, since that means we guarantee that the user receives at least _amount_ of B
get_next_sqrt_price_from_b_round_down(
sqrt_price,
liquidity,
amount,
amount_specified_is_input,
)
}
}
#[cfg(test)]
mod fuzz_tests {
use super::*;
use crate::math::{bit_math::*, tick_math::*, U256};
use proptest::prelude::*;
// Cases where the math overflows or errors
//
// get_next_sqrt_price_from_a_round_up
// sqrt_price_new = (sqrt_price * liquidity) / (liquidity + amount * sqrt_price)
//
// If amount_specified_is_input == false
// DivideByZero: (liquidity / liquidity - amount * sqrt_price)
// liquidity <= sqrt_price * amount, divide by zero error
// TokenMax/MinExceed
// (sqrt_price * liquidity) / (liquidity + amount * sqrt_price) > 2^32 - 1
//
// get_next_sqrt_price_from_b_round_down
// SqrtPriceOutOfBounds
// sqrt_price - (amount / liquidity) < 0
//
// get_amount_delta_b
// TokenMaxExceeded
// (price_1 - price_0) * liquidity > 2^64
proptest! {
#[test]
fn test_get_next_sqrt_price_from_a_round_up (
sqrt_price in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
liquidity in 1..u128::MAX,
amount in 0..u64::MAX,
) {
prop_assume!(sqrt_price != 0);
// Case 1. amount_specified_is_input = true, a_to_b = true
// We are adding token A to the supply, causing price to decrease (Eq 1.)
// Since we are fixing input, we can not exceed the amount that is being provided by the user.
// Because a higher price is inversely correlated with an increased supply of A,
// a higher price means we are adding less A. Thus when performing math, we wish to round the
// price up, since that means that we are guaranteed to not exceed the fixed amount of A provided
let case_1_price = get_next_sqrt_price_from_a_round_up(sqrt_price, liquidity, amount, true);
if liquidity.leading_zeros() + sqrt_price.leading_zeros() < Q64_RESOLUTION.into() {
assert!(case_1_price.is_err());
} else {
assert!(amount >= get_amount_delta_a(sqrt_price, case_1_price?, liquidity, true)?);
// Case 2. amount_specified_is_input = false, a_to_b = false
// We are removing token A from the supply, causing price to increase (Eq 1.)
// Since we are fixing output, we want to guarantee that the user is provided at least _amount_ of A
// Because a higher price is correlated with a decreased supply of A,
// a higher price means we are removing more A to give to the user. Thus when performing math, we wish
// to round the price up, since that means we guarantee that user receives at least _amount_ of A
let case_2_price = get_next_sqrt_price_from_a_round_up(sqrt_price, liquidity, amount, false);
// We need to expand into U256 space here in order to support large enough values
// Q64 << 64 => Q64.64
let liquidity_x64 = U256::from(liquidity) << Q64_RESOLUTION;
// Q64.64 * Q64 => Q128.64
let product = U256::from(sqrt_price) * U256::from(amount);
if liquidity_x64 <= product {
assert!(case_2_price.is_err());
} else {
assert!(amount <= get_amount_delta_a(sqrt_price, case_2_price?, liquidity, false)?);
assert!(case_2_price? >= sqrt_price);
}
if amount == 0 {
assert!(case_1_price? == case_2_price?);
}
}
}
#[test]
fn test_get_next_sqrt_price_from_b_round_down (
sqrt_price in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
liquidity in 1..u128::MAX,
amount in 0..u64::MAX,
) {
prop_assume!(sqrt_price != 0);
// Case 3. amount_specified_is_input = true, a_to_b = false
// We are adding token B to the supply, causing price to increase (Eq 1.)
// Since we are fixing input, we can not exceed the amount that is being provided by the user.
// Because a lower price is inversely correlated with an increased supply of B,
// a lower price means that we are adding less B. Thus when performing math, we wish to round the
// price down, since that means that we are guaranteed to not exceed the fixed amount of B provided.
let case_3_price = get_next_sqrt_price_from_b_round_down(sqrt_price, liquidity, amount, true)?;
assert!(case_3_price >= sqrt_price);
assert!(amount >= get_amount_delta_b(sqrt_price, case_3_price, liquidity, true)?);
// Case 4. amount_specified_is_input = false, a_to_b = true
// We are removing token B from the supply, causing price to decrease (Eq 1.)
// Since we are fixing output, we want to guarantee that the user is provided at least _amount_ of B
// Because a lower price is correlated with a decreased supply of B,
// a lower price means we are removing more B to give to the user. Thus when performing math, we
// wish to round the price down, since that means we guarantee that the user receives at least _amount_ of B
let case_4_price = get_next_sqrt_price_from_b_round_down(sqrt_price, liquidity, amount, false);
// Q64.0 << 64 => Q64.64
let amount_x64 = u128::from(amount) << Q64_RESOLUTION;
let delta = div_round_up(amount_x64, liquidity.into())?;
if sqrt_price < delta {
// In Case 4, error if sqrt_price < delta
assert!(case_4_price.is_err());
} else {
let calc_delta = get_amount_delta_b(sqrt_price, case_4_price?, liquidity, false);
if calc_delta.is_ok() {
assert!(amount <= calc_delta?);
}
// In Case 4, price is decreasing
assert!(case_4_price? <= sqrt_price);
}
if amount == 0 {
assert!(case_3_price == case_4_price?);
}
}
#[test]
fn test_get_amount_delta_a(
sqrt_price_0 in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
sqrt_price_1 in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
liquidity in 0..u128::MAX,
) {
let (sqrt_price_lower, sqrt_price_upper) = increasing_price_order(sqrt_price_0, sqrt_price_1);
let rounded = get_amount_delta_a(sqrt_price_0, sqrt_price_1, liquidity, true);
if liquidity.leading_zeros() + (sqrt_price_upper - sqrt_price_lower).leading_zeros() < Q64_RESOLUTION.into() {
assert!(rounded.is_err())
} else {
let unrounded = get_amount_delta_a(sqrt_price_0, sqrt_price_1, liquidity, false)?;
// Price difference symmetry
assert_eq!(rounded?, get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, true)?);
assert_eq!(unrounded, get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, false)?);
// Rounded should always be larger
assert!(unrounded <= rounded?);
// Diff should be no more than 1
assert!(rounded? - unrounded <= 1);
}
}
#[test]
fn test_get_amount_delta_b(
sqrt_price_0 in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
sqrt_price_1 in MIN_SQRT_PRICE_X64..MAX_SQRT_PRICE_X64,
liquidity in 0..u128::MAX,
) {
let (price_lower, price_upper) = increasing_price_order(sqrt_price_0, sqrt_price_1);
// We need 256 here since we may end up above u128 bits
let n_0 = U256::from(liquidity); // Q64.0, not using 64 MSB
let n_1 = U256::from(price_upper - price_lower); // Q32.64 - Q32.64 => Q32.64
// Shift by 64 in order to remove fractional bits
let m = n_0 * n_1; // Q64.0 * Q32.64 => Q96.64
let delta = m >> Q64_RESOLUTION; // Q96.64 >> 64 => Q96.0
let has_mod = m % TO_Q64 > U256::zero();
let round_up_delta = if has_mod { delta + U256::from(1) } else { delta };
let rounded = get_amount_delta_b(sqrt_price_0, sqrt_price_1, liquidity, true);
let unrounded = get_amount_delta_b(sqrt_price_0, sqrt_price_1, liquidity, false);
let u64_max_in_u256 = U256::from(u64::MAX);
if delta > u64_max_in_u256 {
assert!(rounded.is_err());
assert!(unrounded.is_err());
} else if round_up_delta > u64_max_in_u256 {
assert!(rounded.is_err());
// Price symmmetry
assert_eq!(unrounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false)?);
} else {
// Price difference symmetry
assert_eq!(rounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, true)?);
assert_eq!(unrounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false)?);
// Rounded should always be larger
assert!(unrounded? <= rounded? );
// Diff should be no more than 1
assert!(rounded? - unrounded? <= 1);
}
}
}
}
#[cfg(test)]
mod test_get_amount_delta {
// Δt_a = ((liquidity * (sqrt_price_lower - sqrt_price_upper)) / sqrt_price_upper) / sqrt_price_lower
use super::get_amount_delta_a;
use super::get_amount_delta_b;
#[test]
fn test_get_amount_delta_ok() {
// A
assert_eq!(get_amount_delta_a(4 << 64, 2 << 64, 4, true).unwrap(), 1);
assert_eq!(get_amount_delta_a(4 << 64, 2 << 64, 4, false).unwrap(), 1);
// B
assert_eq!(get_amount_delta_b(4 << 64, 2 << 64, 4, true).unwrap(), 8);
assert_eq!(get_amount_delta_b(4 << 64, 2 << 64, 4, false).unwrap(), 8);
}
#[test]
fn test_get_amount_delta_price_diff_zero_ok() {
// A
assert_eq!(get_amount_delta_a(4 << 64, 4 << 64, 4, true).unwrap(), 0);
assert_eq!(get_amount_delta_a(4 << 64, 4 << 64, 4, false).unwrap(), 0);
// B
assert_eq!(get_amount_delta_b(4 << 64, 4 << 64, 4, true).unwrap(), 0);
assert_eq!(get_amount_delta_b(4 << 64, 4 << 64, 4, false).unwrap(), 0);
}
#[test]
fn test_get_amount_delta_a_overflow() {
assert!(get_amount_delta_a(1 << 64, 2 << 64, u128::MAX, true).is_err());
assert!(get_amount_delta_a(1 << 64, 2 << 64, (u64::MAX as u128) << 1 + 1, true).is_err());
assert!(get_amount_delta_a(1 << 64, 2 << 64, (u64::MAX as u128) << 1, true).is_ok());
assert!(get_amount_delta_a(1 << 64, 2 << 64, u64::MAX as u128, true).is_ok());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
use anchor_lang::prelude::*;
use crate::{errors::ErrorCode, math::MAX_PROTOCOL_FEE_RATE};
#[account]
pub struct WhirlpoolsConfig {
pub fee_authority: Pubkey,
pub collect_protocol_fees_authority: Pubkey,
pub reward_emissions_super_authority: Pubkey,
pub default_protocol_fee_rate: u16,
}
impl WhirlpoolsConfig {
pub const LEN: usize = 8 + 96 + 4;
pub fn update_fee_authority(&mut self, fee_authority: Pubkey) {
self.fee_authority = fee_authority;
}
pub fn update_collect_protocol_fees_authority(
&mut self,
collect_protocol_fees_authority: Pubkey,
) {
self.collect_protocol_fees_authority = collect_protocol_fees_authority;
}
pub fn initialize(
&mut self,
fee_authority: Pubkey,
collect_protocol_fees_authority: Pubkey,
reward_emissions_super_authority: Pubkey,
default_protocol_fee_rate: u16,
) -> Result<(), ErrorCode> {
self.fee_authority = fee_authority;
self.collect_protocol_fees_authority = collect_protocol_fees_authority;
self.reward_emissions_super_authority = reward_emissions_super_authority;
self.update_default_protocol_fee_rate(default_protocol_fee_rate)?;
Ok(())
}
pub fn update_reward_emissions_super_authority(
&mut self,
reward_emissions_super_authority: Pubkey,
) {
self.reward_emissions_super_authority = reward_emissions_super_authority;
}
pub fn update_default_protocol_fee_rate(
&mut self,
default_protocol_fee_rate: u16,
) -> Result<(), ErrorCode> {
if default_protocol_fee_rate > MAX_PROTOCOL_FEE_RATE {
return Err(ErrorCode::ProtocolFeeRateMaxExceeded.into());
}
self.default_protocol_fee_rate = default_protocol_fee_rate;
Ok(())
}
}

View File

@ -0,0 +1,35 @@
use crate::state::WhirlpoolsConfig;
use crate::{errors::ErrorCode, math::MAX_FEE_RATE};
use anchor_lang::prelude::*;
#[account]
pub struct FeeTier {
pub whirlpools_config: Pubkey,
pub tick_spacing: u16,
pub default_fee_rate: u16,
}
impl FeeTier {
pub const LEN: usize = 8 + 32 + 4;
pub fn initialize(
&mut self,
whirlpools_config: &Account<WhirlpoolsConfig>,
tick_spacing: u16,
default_fee_rate: u16,
) -> Result<(), ErrorCode> {
self.whirlpools_config = whirlpools_config.key();
self.tick_spacing = tick_spacing;
self.update_default_fee_rate(default_fee_rate)?;
Ok(())
}
pub fn update_default_fee_rate(&mut self, default_fee_rate: u16) -> Result<(), ErrorCode> {
if default_fee_rate > MAX_FEE_RATE {
return Err(ErrorCode::FeeRateMaxExceeded.into());
}
self.default_fee_rate = default_fee_rate;
Ok(())
}
}

View File

@ -0,0 +1,11 @@
pub mod config;
pub mod fee_tier;
pub mod position;
pub mod tick;
pub mod whirlpool;
pub use self::whirlpool::*;
pub use config::*;
pub use fee_tier::*;
pub use position::*;
pub use tick::*;

View File

@ -0,0 +1,276 @@
use anchor_lang::prelude::*;
use crate::{errors::ErrorCode, state::NUM_REWARDS};
use super::{Tick, Whirlpool};
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Copy)]
pub struct OpenPositionBumps {
pub position_bump: u8,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Copy)]
pub struct OpenPositionWithMetadataBumps {
pub position_bump: u8,
pub metadata_bump: u8,
}
#[account]
#[derive(Default)]
pub struct Position {
pub whirlpool: Pubkey, // 32
pub position_mint: Pubkey, // 32
pub liquidity: u128, // 16
pub tick_lower_index: i32, // 4
pub tick_upper_index: i32, // 4
// Q64.64
pub fee_growth_checkpoint_a: u128, // 16
pub fee_owed_a: u64, // 8
// Q64.64
pub fee_growth_checkpoint_b: u128, // 16
pub fee_owed_b: u64, // 8
pub reward_infos: [PositionRewardInfo; NUM_REWARDS], // 72
}
impl Position {
pub const LEN: usize = 8 + 136 + 72;
pub fn is_position_empty<'info>(position: &Position) -> bool {
let fees_not_owed = position.fee_owed_a == 0 && position.fee_owed_b == 0;
let mut rewards_not_owed = true;
for i in 0..NUM_REWARDS {
rewards_not_owed = rewards_not_owed && position.reward_infos[i].amount_owed == 0
}
position.liquidity == 0 && fees_not_owed && rewards_not_owed
}
pub fn update(&mut self, update: &PositionUpdate) {
self.liquidity = update.liquidity;
self.fee_growth_checkpoint_a = update.fee_growth_checkpoint_a;
self.fee_growth_checkpoint_b = update.fee_growth_checkpoint_b;
self.fee_owed_a = update.fee_owed_a;
self.fee_owed_b = update.fee_owed_b;
self.reward_infos = update.reward_infos;
}
pub fn open_position(
&mut self,
whirlpool: &Account<Whirlpool>,
position_mint: Pubkey,
tick_lower_index: i32,
tick_upper_index: i32,
) -> Result<(), ErrorCode> {
if !Tick::check_is_usable_tick(tick_lower_index, whirlpool.tick_spacing)
|| !Tick::check_is_usable_tick(tick_upper_index, whirlpool.tick_spacing)
|| tick_lower_index >= tick_upper_index
{
return Err(ErrorCode::InvalidTickIndex.into());
}
self.whirlpool = whirlpool.key();
self.position_mint = position_mint;
self.tick_lower_index = tick_lower_index;
self.tick_upper_index = tick_upper_index;
Ok(())
}
pub fn reset_fees_owed(&mut self) {
self.fee_owed_a = 0;
self.fee_owed_b = 0;
}
pub fn update_reward_owed(&mut self, index: usize, amount_owed: u64) {
self.reward_infos[index].amount_owed = amount_owed;
}
}
#[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize, Default, Debug, PartialEq)]
pub struct PositionRewardInfo {
// Q64.64
pub growth_inside_checkpoint: u128,
pub amount_owed: u64,
}
#[derive(Default, Debug, PartialEq)]
pub struct PositionUpdate {
pub liquidity: u128,
pub fee_growth_checkpoint_a: u128,
pub fee_owed_a: u64,
pub fee_growth_checkpoint_b: u128,
pub fee_owed_b: u64,
pub reward_infos: [PositionRewardInfo; NUM_REWARDS],
}
#[cfg(test)]
mod is_position_empty_tests {
use super::*;
use crate::constants::test_constants::*;
pub fn build_test_position(
liquidity: u128,
fee_owed_a: u64,
fee_owed_b: u64,
reward_owed_0: u64,
reward_owed_1: u64,
reward_owed_2: u64,
) -> Position {
Position {
whirlpool: test_program_id(),
position_mint: test_program_id(),
liquidity,
tick_lower_index: 0,
tick_upper_index: 0,
fee_growth_checkpoint_a: 0,
fee_owed_a,
fee_growth_checkpoint_b: 0,
fee_owed_b,
reward_infos: [
PositionRewardInfo {
growth_inside_checkpoint: 0,
amount_owed: reward_owed_0,
},
PositionRewardInfo {
growth_inside_checkpoint: 0,
amount_owed: reward_owed_1,
},
PositionRewardInfo {
growth_inside_checkpoint: 0,
amount_owed: reward_owed_2,
},
],
}
}
#[test]
fn test_position_empty() {
let pos = build_test_position(0, 0, 0, 0, 0, 0);
assert_eq!(Position::is_position_empty(&pos), true);
}
#[test]
fn test_liquidity_non_zero() {
let pos = build_test_position(100, 0, 0, 0, 0, 0);
assert_eq!(Position::is_position_empty(&pos), false);
}
#[test]
fn test_fee_a_non_zero() {
let pos = build_test_position(0, 100, 0, 0, 0, 0);
assert_eq!(Position::is_position_empty(&pos), false);
}
#[test]
fn test_fee_b_non_zero() {
let pos = build_test_position(0, 0, 100, 0, 0, 0);
assert_eq!(Position::is_position_empty(&pos), false);
}
#[test]
fn test_reward_0_non_zero() {
let pos = build_test_position(0, 0, 0, 100, 0, 0);
assert_eq!(Position::is_position_empty(&pos), false);
}
#[test]
fn test_reward_1_non_zero() {
let pos = build_test_position(0, 0, 0, 0, 100, 0);
assert_eq!(Position::is_position_empty(&pos), false);
}
#[test]
fn test_reward_2_non_zero() {
let pos = build_test_position(0, 0, 0, 0, 0, 100);
assert_eq!(Position::is_position_empty(&pos), false);
}
}
#[cfg(test)]
pub mod position_builder {
use anchor_lang::prelude::Pubkey;
use super::{Position, PositionRewardInfo};
use crate::state::NUM_REWARDS;
#[derive(Default)]
pub struct PositionBuilder {
liquidity: u128,
tick_lower_index: i32,
tick_upper_index: i32,
// Q64.64
fee_growth_checkpoint_a: u128,
fee_owed_a: u64,
// Q64.64
fee_growth_checkpoint_b: u128,
fee_owed_b: u64,
// Size should equal state::NUM_REWARDS
reward_infos: [PositionRewardInfo; NUM_REWARDS],
}
impl PositionBuilder {
pub fn new(tick_lower_index: i32, tick_upper_index: i32) -> Self {
Self {
tick_lower_index,
tick_upper_index,
reward_infos: [PositionRewardInfo::default(); NUM_REWARDS],
..Default::default()
}
}
pub fn liquidity(mut self, liquidity: u128) -> Self {
self.liquidity = liquidity;
self
}
pub fn fee_growth_checkpoint_a(mut self, fee_growth_checkpoint_a: u128) -> Self {
self.fee_growth_checkpoint_a = fee_growth_checkpoint_a;
self
}
pub fn fee_growth_checkpoint_b(mut self, fee_growth_checkpoint_b: u128) -> Self {
self.fee_growth_checkpoint_b = fee_growth_checkpoint_b;
self
}
pub fn fee_owed_a(mut self, fee_owed_a: u64) -> Self {
self.fee_owed_a = fee_owed_a;
self
}
pub fn fee_owed_b(mut self, fee_owed_b: u64) -> Self {
self.fee_owed_b = fee_owed_b;
self
}
pub fn reward_info(mut self, index: usize, reward_info: PositionRewardInfo) -> Self {
self.reward_infos[index] = reward_info;
self
}
pub fn reward_infos(mut self, reward_infos: [PositionRewardInfo; NUM_REWARDS]) -> Self {
self.reward_infos = reward_infos;
self
}
pub fn build(self) -> Position {
Position {
whirlpool: Pubkey::new_unique(),
position_mint: Pubkey::new_unique(),
liquidity: self.liquidity,
fee_growth_checkpoint_a: self.fee_growth_checkpoint_a,
fee_growth_checkpoint_b: self.fee_growth_checkpoint_b,
fee_owed_a: self.fee_owed_a,
fee_owed_b: self.fee_owed_b,
reward_infos: self.reward_infos,
tick_lower_index: self.tick_lower_index,
tick_upper_index: self.tick_upper_index,
..Default::default()
}
}
}
}

View File

@ -0,0 +1,624 @@
use crate::errors::ErrorCode;
use crate::state::NUM_REWARDS;
use anchor_lang::prelude::*;
use super::Whirlpool;
// Max & min tick index based on sqrt(1.0001) & max.min price of 2^64
pub const MAX_TICK_INDEX: i32 = 443636;
pub const MIN_TICK_INDEX: i32 = -443636;
// We have two consts because most of our code uses it as a i32. However,
// for us to use it in tick array declarations, anchor requires it to be a usize.
pub const TICK_ARRAY_SIZE: i32 = 88;
pub const TICK_ARRAY_SIZE_USIZE: usize = 88;
#[zero_copy]
#[repr(packed)]
#[derive(Default, Debug, PartialEq)]
pub struct Tick {
// Total 137 bytes
pub initialized: bool, // 1
pub liquidity_net: i128, // 16
pub liquidity_gross: u128, // 16
// Q64.64
pub fee_growth_outside_a: u128, // 16
// Q64.64
pub fee_growth_outside_b: u128, // 16
// Array of Q64.64
pub reward_growths_outside: [u128; NUM_REWARDS], // 48 = 16 * 3
}
impl Tick {
pub const LEN: usize = 113;
/// Apply an update for this tick
///
/// # Parameters
/// - `update` - An update object to update the values in this tick
pub fn update(&mut self, update: &TickUpdate) {
self.initialized = update.initialized;
self.liquidity_net = update.liquidity_net;
self.liquidity_gross = update.liquidity_gross;
self.fee_growth_outside_a = update.fee_growth_outside_a;
self.fee_growth_outside_b = update.fee_growth_outside_b;
self.reward_growths_outside = update.reward_growths_outside;
}
/// Check that the tick index is within the supported range of this contract
///
/// # Parameters
/// - `tick_index` - A i32 integer representing the tick index
///
/// # Returns
/// - `true`: The tick index is not within the range supported by this contract
/// - `false`: The tick index is within the range supported by this contract
pub fn check_is_out_of_bounds(tick_index: i32) -> bool {
tick_index > MAX_TICK_INDEX || tick_index < MIN_TICK_INDEX
}
/// Check that the tick index is a valid start tick for a tick array in this whirlpool
/// A valid start-tick-index is a multiple of tick_spacing & number of ticks in a tick-array.
///
/// # Parameters
/// - `tick_index` - A i32 integer representing the tick index
/// - `tick_spacing` - A u8 integer of the tick spacing for this whirlpool
///
/// # Returns
/// - `true`: The tick index is a valid start-tick-index for this whirlpool
/// - `false`: The tick index is not a valid start-tick-index for this whirlpool
/// or the tick index not within the range supported by this contract
pub fn check_is_valid_start_tick(tick_index: i32, tick_spacing: u16) -> bool {
let ticks_in_array = TICK_ARRAY_SIZE * tick_spacing as i32;
if Tick::check_is_out_of_bounds(tick_index) {
// Left-edge tick-array can have a start-tick-index smaller than the min tick index
if tick_index > MIN_TICK_INDEX {
return false;
}
let min_array_start_index =
MIN_TICK_INDEX - (MIN_TICK_INDEX % ticks_in_array + ticks_in_array);
return tick_index == min_array_start_index;
}
tick_index % ticks_in_array == 0
}
/// Check that the tick index is within bounds and is a usable tick index for the given tick spacing.
///
/// # Parameters
/// - `tick_index` - A i32 integer representing the tick index
/// - `tick_spacing` - A u8 integer of the tick spacing for this whirlpool
///
/// # Returns
/// - `true`: The tick index is within max/min index bounds for this protocol and is a usable tick-index given the tick-spacing
/// - `false`: The tick index is out of bounds or is not a usable tick for this tick-spacing
pub fn check_is_usable_tick(tick_index: i32, tick_spacing: u16) -> bool {
if Tick::check_is_out_of_bounds(tick_index) {
return false;
}
tick_index % tick_spacing as i32 == 0
}
/// Bound a tick-index value to the max & min index value for this protocol
///
/// # Parameters
/// - `tick_index` - A i32 integer representing the tick index
///
/// # Returns
/// - `i32` The input tick index value but bounded by the max/min value of this protocol.
pub fn bound_tick_index(tick_index: i32) -> i32 {
tick_index.max(MIN_TICK_INDEX).min(MAX_TICK_INDEX)
}
}
#[derive(Default, Debug, PartialEq)]
pub struct TickUpdate {
pub initialized: bool,
pub liquidity_net: i128,
pub liquidity_gross: u128,
pub fee_growth_outside_a: u128,
pub fee_growth_outside_b: u128,
pub reward_growths_outside: [u128; NUM_REWARDS],
}
impl TickUpdate {
pub fn from(tick: &Tick) -> TickUpdate {
TickUpdate {
initialized: tick.initialized,
liquidity_net: tick.liquidity_net,
liquidity_gross: tick.liquidity_gross,
fee_growth_outside_a: tick.fee_growth_outside_a,
fee_growth_outside_b: tick.fee_growth_outside_b,
reward_growths_outside: tick.reward_growths_outside,
}
}
}
#[account(zero_copy)]
#[repr(packed)]
pub struct TickArray {
pub start_tick_index: i32,
pub ticks: [Tick; TICK_ARRAY_SIZE_USIZE],
pub whirlpool: Pubkey,
}
impl Default for TickArray {
#[inline]
fn default() -> TickArray {
TickArray {
whirlpool: Pubkey::default(),
ticks: [Tick::default(); TICK_ARRAY_SIZE_USIZE],
start_tick_index: 0,
}
}
}
impl TickArray {
pub const LEN: usize = 8 + 36 + (Tick::LEN * TICK_ARRAY_SIZE_USIZE);
/// Search for the next initialized tick in this array.
///
/// # Parameters
/// - `tick_index` - A i32 integer representing the tick index to start searching for
/// - `tick_spacing` - A u8 integer of the tick spacing for this whirlpool
/// - `a_to_b` - If the trade is from a_to_b, the search will move to the left and the starting search tick is inclusive.
/// If the trade is from b_to_a, the search will move to the right and the starting search tick is not inclusive.
///
/// # Returns
/// - `Some(i32)`: The next initialized tick index of this array
/// - `None`: An initialized tick index was not found in this array
/// - `InvalidTickArraySequence` - error if `tick_index` is not a valid search tick for the array
/// - `InvalidTickSpacing` - error if the provided tick spacing is 0
pub fn get_next_init_tick_index(
&self,
tick_index: i32,
tick_spacing: u16,
a_to_b: bool,
) -> Result<Option<i32>, ErrorCode> {
if !self.in_search_range(tick_index, tick_spacing, !a_to_b) {
return Err(ErrorCode::InvalidTickArraySequence);
}
let mut curr_offset = match self.tick_offset(tick_index, tick_spacing) {
Ok(value) => value as i32,
Err(e) => return Err(e),
};
// For a_to_b searches, the search moves to the left. The next possible init-tick can be the 1st tick in the current offset
// For b_to_a searches, the search moves to the right. The next possible init-tick cannot be within the current offset
if !a_to_b {
curr_offset += 1;
}
while curr_offset >= 0 && curr_offset < TICK_ARRAY_SIZE {
let curr_tick = self.ticks[curr_offset as usize];
if curr_tick.initialized {
return Ok(Some(
(curr_offset * tick_spacing as i32) + self.start_tick_index,
));
}
curr_offset = if a_to_b {
curr_offset - 1
} else {
curr_offset + 1
};
}
Ok(None)
}
/// Initialize the TickArray object
///
/// # Parameters
/// - `whirlpool` - the tick index the desired Tick object is stored in
/// - `start_tick_index` - A u8 integer of the tick spacing for this whirlpool
///
/// # Errors
/// - `InvalidStartTick`: - The provided start-tick-index is not an initializable tick index in this Whirlpool w/ this tick-spacing.
pub fn initialize(
&mut self,
whirlpool: &Account<Whirlpool>,
start_tick_index: i32,
) -> Result<(), ErrorCode> {
if !Tick::check_is_valid_start_tick(start_tick_index, whirlpool.tick_spacing) {
return Err(ErrorCode::InvalidStartTick.into());
}
self.whirlpool = whirlpool.key();
self.start_tick_index = start_tick_index;
Ok(())
}
/// Get the Tick object at the given tick-index & tick-spacing
///
/// # Parameters
/// - `tick_index` - the tick index the desired Tick object is stored in
/// - `tick_spacing` - A u8 integer of the tick spacing for this whirlpool
///
/// # Returns
/// - `&Tick`: A reference to the desired Tick object
/// - `TickNotFound`: - The provided tick-index is not an initializable tick index in this Whirlpool w/ this tick-spacing.
pub fn get_tick(&self, tick_index: i32, tick_spacing: u16) -> Result<&Tick, ErrorCode> {
if !self.check_in_array_bounds(tick_index, tick_spacing)
|| !Tick::check_is_usable_tick(tick_index, tick_spacing)
{
return Err(ErrorCode::TickNotFound);
}
let offset = self.tick_offset(tick_index, tick_spacing)?;
if offset < 0 {
return Err(ErrorCode::TickNotFound);
}
Ok(&self.ticks[offset as usize])
}
/// Updates the Tick object at the given tick-index & tick-spacing
///
/// # Parameters
/// - `tick_index` - the tick index the desired Tick object is stored in
/// - `tick_spacing` - A u8 integer of the tick spacing for this whirlpool
/// - `update` - A reference to a TickUpdate object to update the Tick object at the given index
///
/// # Errors
/// - `TickNotFound`: - The provided tick-index is not an initializable tick index in this Whirlpool w/ this tick-spacing.
pub fn update_tick(
&mut self,
tick_index: i32,
tick_spacing: u16,
update: &TickUpdate,
) -> Result<(), ErrorCode> {
if !self.check_in_array_bounds(tick_index, tick_spacing)
|| !Tick::check_is_usable_tick(tick_index, tick_spacing)
{
return Err(ErrorCode::TickNotFound);
}
let offset = self.tick_offset(tick_index, tick_spacing)?;
if offset < 0 {
return Err(ErrorCode::TickNotFound);
}
self.ticks.get_mut(offset as usize).unwrap().update(update);
Ok(())
}
/// Checks that this array holds the next tick index for the current tick index, given the pool's tick spacing & search direction.
///
/// unshifted checks on [start, start + TICK_ARRAY_SIZE * tick_spacing)
/// shifted checks on [start - tick_spacing, start + (TICK_ARRAY_SIZE - 1) * tick_spacing) (adjusting range by -tick_spacing)
///
/// shifted == !a_to_b
///
/// For a_to_b swaps, price moves left. All searchable ticks in this tick-array's range will end up in this tick's usable ticks.
/// The search range is therefore the range of the tick-array.
///
/// For b_to_a swaps, this tick-array's left-most ticks can be the 'next' usable tick-index of the previous tick-array.
/// The right-most ticks also points towards the next tick-array. The search range is therefore shifted by 1 tick-spacing.
pub fn in_search_range(&self, tick_index: i32, tick_spacing: u16, shifted: bool) -> bool {
let mut lower = self.start_tick_index;
let mut upper = self.start_tick_index + TICK_ARRAY_SIZE * tick_spacing as i32;
if shifted {
lower = lower - tick_spacing as i32;
upper = upper - tick_spacing as i32;
}
tick_index >= lower && tick_index < upper
}
pub fn check_in_array_bounds(&self, tick_index: i32, tick_spacing: u16) -> bool {
self.in_search_range(tick_index, tick_spacing, false)
}
pub fn is_min_tick_array(&self) -> bool {
self.start_tick_index <= MIN_TICK_INDEX
}
pub fn is_max_tick_array(&self, tick_spacing: u16) -> bool {
self.start_tick_index + TICK_ARRAY_SIZE * (tick_spacing as i32) > MAX_TICK_INDEX
}
// Calculates an offset from a tick index that can be used to access the tick data
pub fn tick_offset(&self, tick_index: i32, tick_spacing: u16) -> Result<isize, ErrorCode> {
if tick_spacing == 0 {
return Err(ErrorCode::InvalidTickSpacing);
}
Ok(get_offset(tick_index, self.start_tick_index, tick_spacing))
}
}
fn get_offset(tick_index: i32, start_tick_index: i32, tick_spacing: u16) -> isize {
// TODO: replace with i32.div_floor once not experimental
let lhs = tick_index - start_tick_index;
let rhs = tick_spacing as i32;
let d = lhs / rhs;
let r = lhs % rhs;
let o = if (r > 0 && rhs < 0) || (r < 0 && rhs > 0) {
d - 1
} else {
d
};
return o as isize;
}
#[cfg(test)]
pub mod tick_builder {
use super::Tick;
use crate::state::NUM_REWARDS;
#[derive(Default)]
pub struct TickBuilder {
initialized: bool,
liquidity_net: i128,
liquidity_gross: u128,
fee_growth_outside_a: u128,
fee_growth_outside_b: u128,
reward_growths_outside: [u128; NUM_REWARDS],
}
impl TickBuilder {
pub fn initialized(mut self, initialized: bool) -> Self {
self.initialized = initialized;
self
}
pub fn liquidity_net(mut self, liquidity_net: i128) -> Self {
self.liquidity_net = liquidity_net;
self
}
pub fn liquidity_gross(mut self, liquidity_gross: u128) -> Self {
self.liquidity_gross = liquidity_gross;
self
}
pub fn fee_growth_outside_a(mut self, fee_growth_outside_a: u128) -> Self {
self.fee_growth_outside_a = fee_growth_outside_a;
self
}
pub fn fee_growth_outside_b(mut self, fee_growth_outside_b: u128) -> Self {
self.fee_growth_outside_b = fee_growth_outside_b;
self
}
pub fn reward_growths_outside(
mut self,
reward_growths_outside: [u128; NUM_REWARDS],
) -> Self {
self.reward_growths_outside = reward_growths_outside;
self
}
pub fn build(self) -> Tick {
Tick {
initialized: self.initialized,
liquidity_net: self.liquidity_net,
liquidity_gross: self.liquidity_gross,
fee_growth_outside_a: self.fee_growth_outside_a,
fee_growth_outside_b: self.fee_growth_outside_b,
reward_growths_outside: self.reward_growths_outside,
}
}
}
}
#[cfg(test)]
mod fuzz_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_get_search_and_offset(
tick_index in 2 * MIN_TICK_INDEX..2 * MAX_TICK_INDEX,
start_tick_index in 2 * MIN_TICK_INDEX..2 * MAX_TICK_INDEX,
tick_spacing in 1u16..u16::MAX,
a_to_b in proptest::bool::ANY,
) {
let mut array = TickArray::default();
array.start_tick_index = start_tick_index;
let in_search = array.in_search_range(tick_index, tick_spacing, !a_to_b);
let mut lower_bound = start_tick_index;
let mut upper_bound = start_tick_index + TICK_ARRAY_SIZE * tick_spacing as i32;
let mut offset_lower = 0;
let mut offset_upper = TICK_ARRAY_SIZE as isize;
// If we are doing b_to_a, we shift the index bounds by -tick_spacing
// and the offset bounds by -1
if !a_to_b {
lower_bound = lower_bound - tick_spacing as i32;
upper_bound = upper_bound - tick_spacing as i32;
offset_lower = -1;
offset_upper = offset_upper - 1;
}
// in_bounds should be identical to search
let in_bounds = tick_index >= lower_bound && tick_index < upper_bound;
assert!(in_bounds == in_search);
if in_search {
let offset = get_offset(tick_index, start_tick_index, tick_spacing);
assert!(offset >= offset_lower && offset < offset_upper)
}
}
#[test]
fn test_get_offset(
tick_index in 2 * MIN_TICK_INDEX..2 * MAX_TICK_INDEX,
start_tick_index in 2 * MIN_TICK_INDEX..2 * MAX_TICK_INDEX,
tick_spacing in 1u16..u16::MAX,
) {
let offset = get_offset(tick_index, start_tick_index, tick_spacing);
let rounded = start_tick_index >= tick_index;
let raw = (tick_index - start_tick_index) / tick_spacing as i32;
let d = raw as isize;
if !rounded {
assert_eq!(offset, d);
} else {
assert!(offset == d || offset == (raw - 1) as isize);
}
}
}
}
#[cfg(test)]
mod check_is_valid_start_tick_tests {
use super::*;
const TS_8: u16 = 8;
const TS_128: u16 = 128;
#[test]
fn test_start_tick_is_zero() {
assert_eq!(Tick::check_is_valid_start_tick(0, TS_8), true);
}
#[test]
fn test_start_tick_is_valid_ts8() {
assert_eq!(Tick::check_is_valid_start_tick(704, TS_8), true);
}
#[test]
fn test_start_tick_is_valid_ts128() {
assert_eq!(Tick::check_is_valid_start_tick(337920, TS_128), true);
}
#[test]
fn test_start_tick_is_valid_negative_ts8() {
assert_eq!(Tick::check_is_valid_start_tick(-704, TS_8), true);
}
#[test]
fn test_start_tick_is_valid_negative_ts128() {
assert_eq!(Tick::check_is_valid_start_tick(-337920, TS_128), true);
}
#[test]
fn test_start_tick_is_not_valid_ts8() {
assert_eq!(Tick::check_is_valid_start_tick(2353573, TS_8), false);
}
#[test]
fn test_start_tick_is_not_valid_ts128() {
assert_eq!(Tick::check_is_valid_start_tick(-2353573, TS_128), false);
}
#[test]
fn test_min_tick_array_start_tick_is_valid_ts8() {
let expected_array_index: i32 = (MIN_TICK_INDEX / TICK_ARRAY_SIZE / TS_8 as i32) - 1;
let expected_start_index_for_last_array: i32 =
expected_array_index * TICK_ARRAY_SIZE * TS_8 as i32;
assert_eq!(
Tick::check_is_valid_start_tick(expected_start_index_for_last_array, TS_8),
true
)
}
#[test]
fn test_min_tick_array_sub_1_start_tick_is_invalid_ts8() {
let expected_array_index: i32 = (MIN_TICK_INDEX / TICK_ARRAY_SIZE / TS_8 as i32) - 2;
let expected_start_index_for_last_array: i32 =
expected_array_index * TICK_ARRAY_SIZE * TS_8 as i32;
assert_eq!(
Tick::check_is_valid_start_tick(expected_start_index_for_last_array, TS_8),
false
)
}
#[test]
fn test_min_tick_array_start_tick_is_valid_ts128() {
let expected_array_index: i32 = (MIN_TICK_INDEX / TICK_ARRAY_SIZE / TS_128 as i32) - 1;
let expected_start_index_for_last_array: i32 =
expected_array_index * TICK_ARRAY_SIZE * TS_128 as i32;
assert_eq!(
Tick::check_is_valid_start_tick(expected_start_index_for_last_array, TS_128),
true
)
}
#[test]
fn test_min_tick_array_sub_1_start_tick_is_invalid_ts128() {
let expected_array_index: i32 = (MIN_TICK_INDEX / TICK_ARRAY_SIZE / TS_128 as i32) - 2;
let expected_start_index_for_last_array: i32 =
expected_array_index * TICK_ARRAY_SIZE * TS_128 as i32;
assert_eq!(
Tick::check_is_valid_start_tick(expected_start_index_for_last_array, TS_128),
false
)
}
}
#[cfg(test)]
mod check_is_out_of_bounds_tests {
use super::*;
#[test]
fn test_min_tick_index() {
assert_eq!(Tick::check_is_out_of_bounds(MIN_TICK_INDEX), false);
}
#[test]
fn test_max_tick_index() {
assert_eq!(Tick::check_is_out_of_bounds(MAX_TICK_INDEX), false);
}
#[test]
fn test_min_tick_index_sub_1() {
assert_eq!(Tick::check_is_out_of_bounds(MIN_TICK_INDEX - 1), true);
}
#[test]
fn test_max_tick_index_add_1() {
assert_eq!(Tick::check_is_out_of_bounds(MAX_TICK_INDEX + 1), true);
}
}
#[cfg(test)]
mod array_update_tests {
use super::*;
#[test]
fn update_applies_successfully() {
let mut array = TickArray::default();
let tick_index = 8;
let original = Tick {
initialized: true,
liquidity_net: 2525252i128,
liquidity_gross: 2525252u128,
fee_growth_outside_a: 28728282u128,
fee_growth_outside_b: 22528728282u128,
reward_growths_outside: [124272242u128, 1271221u128, 966958u128],
};
array.ticks[1] = original;
let update = TickUpdate {
initialized: true,
liquidity_net: 24128472184712i128,
liquidity_gross: 353873892732u128,
fee_growth_outside_a: 3928372892u128,
fee_growth_outside_b: 12242u128,
reward_growths_outside: [53264u128, 539282u128, 98744u128],
};
let tick_spacing = 8;
array
.update_tick(tick_index, tick_spacing, &update)
.unwrap();
let expected = Tick {
initialized: true,
liquidity_net: 24128472184712i128,
liquidity_gross: 353873892732u128,
fee_growth_outside_a: 3928372892u128,
fee_growth_outside_b: 12242u128,
reward_growths_outside: [53264u128, 539282u128, 98744u128],
};
let result = array.get_tick(tick_index, tick_spacing).unwrap();
assert_eq!(*result, expected);
}
}

View File

@ -0,0 +1,414 @@
use crate::{
errors::ErrorCode,
math::{
tick_index_from_sqrt_price, MAX_FEE_RATE, MAX_PROTOCOL_FEE_RATE, MAX_SQRT_PRICE_X64,
MIN_SQRT_PRICE_X64,
},
};
use anchor_lang::prelude::*;
use super::WhirlpoolsConfig;
#[account]
#[derive(Default)]
pub struct Whirlpool {
pub whirlpools_config: Pubkey, // 32
pub whirlpool_bump: [u8; 1], // 1
pub tick_spacing: u16, // 2
pub tick_spacing_seed: [u8; 2], // 2
// Stored as hundredths of a basis point
// u16::MAX corresponds to ~6.5%
pub fee_rate: u16, // 2
// Denominator for portion of fee rate taken (1/x)%
pub protocol_fee_rate: u16, // 2
// Maximum amount that can be held by Solana account
pub liquidity: u128, // 16
// MAX/MIN at Q32.64, but using Q64.64 for rounder bytes
// Q64.64
pub sqrt_price: u128, // 16
pub tick_current_index: i32, // 4
pub protocol_fee_owed_a: u64, // 8
pub protocol_fee_owed_b: u64, // 8
pub token_mint_a: Pubkey, // 32
pub token_vault_a: Pubkey, // 32
// Q64.64
pub fee_growth_global_a: u128, // 16
pub token_mint_b: Pubkey, // 32
pub token_vault_b: Pubkey, // 32
// Q64.64
pub fee_growth_global_b: u128, // 16
pub reward_last_updated_timestamp: u64, // 8
pub reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS], // 384
}
// Number of rewards supported by Whirlpools
pub const NUM_REWARDS: usize = 3;
impl Whirlpool {
pub const LEN: usize = 8 + 261 + 384;
pub fn seeds(&self) -> [&[u8]; 6] {
[
&b"whirlpool"[..],
self.whirlpools_config.as_ref(),
self.token_mint_a.as_ref(),
self.token_mint_b.as_ref(),
self.tick_spacing_seed.as_ref(),
self.whirlpool_bump.as_ref(),
]
}
pub fn initialize(
&mut self,
whirlpools_config: &Account<WhirlpoolsConfig>,
bump: u8,
tick_spacing: u16,
sqrt_price: u128,
default_fee_rate: u16,
token_mint_a: Pubkey,
token_vault_a: Pubkey,
token_mint_b: Pubkey,
token_vault_b: Pubkey,
) -> Result<(), ErrorCode> {
if token_mint_a.ge(&token_mint_b) {
return Err(ErrorCode::InvalidTokenMintOrder.into());
}
if sqrt_price < MIN_SQRT_PRICE_X64 || sqrt_price > MAX_SQRT_PRICE_X64 {
return Err(ErrorCode::SqrtPriceOutOfBounds.into());
}
self.whirlpools_config = whirlpools_config.key();
self.whirlpool_bump = [bump];
self.tick_spacing = tick_spacing;
self.tick_spacing_seed = self.tick_spacing.to_le_bytes();
self.update_fee_rate(default_fee_rate)?;
self.update_protocol_fee_rate(whirlpools_config.default_protocol_fee_rate)?;
self.liquidity = 0;
self.sqrt_price = sqrt_price;
self.tick_current_index = tick_index_from_sqrt_price(&sqrt_price);
self.protocol_fee_owed_a = 0;
self.protocol_fee_owed_b = 0;
self.token_mint_a = token_mint_a;
self.token_vault_a = token_vault_a;
self.fee_growth_global_a = 0;
self.token_mint_b = token_mint_b;
self.token_vault_b = token_vault_b;
self.fee_growth_global_b = 0;
self.reward_infos =
[WhirlpoolRewardInfo::new(whirlpools_config.reward_emissions_super_authority);
NUM_REWARDS];
Ok(())
}
/// Update all reward values for the Whirlpool.
///
/// # Parameters
/// - `reward_infos` - An array of all updated whirlpool rewards
/// - `reward_last_updated_timestamp` - The timestamp when the rewards were last updated
pub fn update_rewards(
&mut self,
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
reward_last_updated_timestamp: u64,
) {
self.reward_last_updated_timestamp = reward_last_updated_timestamp;
self.reward_infos = reward_infos;
}
pub fn update_rewards_and_liquidity(
&mut self,
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
liquidity: u128,
reward_last_updated_timestamp: u64,
) {
self.update_rewards(reward_infos, reward_last_updated_timestamp);
self.liquidity = liquidity;
}
/// Update the reward authority at the specified Whirlpool reward index.
pub fn update_reward_authority(
&mut self,
index: usize,
authority: Pubkey,
) -> Result<(), ErrorCode> {
if index >= NUM_REWARDS {
return Err(ErrorCode::InvalidRewardIndex.into());
}
self.reward_infos[index].authority = authority;
Ok(())
}
pub fn update_emissions(
&mut self,
index: usize,
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
timestamp: u64,
emissions_per_second_x64: u128,
) -> Result<(), ErrorCode> {
if index >= NUM_REWARDS {
return Err(ErrorCode::InvalidRewardIndex.into());
}
self.update_rewards(reward_infos, timestamp);
self.reward_infos[index].emissions_per_second_x64 = emissions_per_second_x64;
Ok(())
}
pub fn initialize_reward(
&mut self,
index: usize,
mint: Pubkey,
vault: Pubkey,
) -> Result<(), ErrorCode> {
if index >= NUM_REWARDS {
return Err(ErrorCode::InvalidRewardIndex.into());
}
let lowest_index = match self.reward_infos.iter().position(|r| !r.initialized()) {
Some(lowest_index) => lowest_index,
None => return Err(ErrorCode::InvalidRewardIndex.into()),
};
if lowest_index != index {
return Err(ErrorCode::InvalidRewardIndex.into());
}
self.reward_infos[index].mint = mint;
self.reward_infos[index].vault = vault;
Ok(())
}
pub fn update_after_swap(
&mut self,
liquidity: u128,
tick_index: i32,
sqrt_price: u128,
fee_growth_global: u128,
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
protocol_fee: u64,
is_token_fee_in_a: bool,
reward_last_updated_timestamp: u64,
) {
self.tick_current_index = tick_index;
self.sqrt_price = sqrt_price;
self.liquidity = liquidity;
self.reward_infos = reward_infos;
self.reward_last_updated_timestamp = reward_last_updated_timestamp;
if is_token_fee_in_a {
// Add fees taken via a
self.fee_growth_global_a = fee_growth_global;
self.protocol_fee_owed_a += protocol_fee;
} else {
// Add fees taken via b
self.fee_growth_global_b = fee_growth_global;
self.protocol_fee_owed_b += protocol_fee;
}
}
pub fn update_fee_rate(&mut self, fee_rate: u16) -> Result<(), ErrorCode> {
if fee_rate > MAX_FEE_RATE {
return Err(ErrorCode::FeeRateMaxExceeded.into());
}
self.fee_rate = fee_rate;
Ok(())
}
pub fn update_protocol_fee_rate(&mut self, protocol_fee_rate: u16) -> Result<(), ErrorCode> {
if protocol_fee_rate > MAX_PROTOCOL_FEE_RATE {
return Err(ErrorCode::ProtocolFeeRateMaxExceeded.into());
}
self.protocol_fee_rate = protocol_fee_rate;
Ok(())
}
pub fn reset_protocol_fees_owed(&mut self) {
self.protocol_fee_owed_a = 0;
self.protocol_fee_owed_b = 0;
}
}
/// Stores the state relevant for tracking liquidity mining rewards at the `Whirlpool` level.
/// These values are used in conjunction with `PositionRewardInfo`, `Tick.reward_growths_outside`,
/// and `Whirlpool.reward_last_updated_timestamp` to determine how many rewards are earned by open
/// positions.
#[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize, Default, Debug, PartialEq)]
pub struct WhirlpoolRewardInfo {
/// Reward token mint.
pub mint: Pubkey,
/// Reward vault token account.
pub vault: Pubkey,
/// Authority account that has permission to initialize the reward and set emissions.
pub authority: Pubkey,
/// Q64.64 number that indicates how many tokens per second are earned per unit of liquidity.
pub emissions_per_second_x64: u128,
/// Q64.64 number that tracks the total tokens earned per unit of liquidity since the reward
/// emissions were turned on.
pub growth_global_x64: u128,
}
impl WhirlpoolRewardInfo {
/// Creates a new `WhirlpoolRewardInfo` with the authority set
pub fn new(authority: Pubkey) -> Self {
Self {
authority,
..Default::default()
}
}
/// Returns true if this reward is initialized.
/// Once initialized, a reward cannot transition back to uninitialized.
pub fn initialized(&self) -> bool {
self.mint.ne(&Pubkey::default())
}
/// Maps all reward data to only the reward growth accumulators
pub fn to_reward_growths(
reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS],
) -> [u128; NUM_REWARDS] {
let mut reward_growths = [0u128; NUM_REWARDS];
for i in 0..NUM_REWARDS {
reward_growths[i] = reward_infos[i].growth_global_x64;
}
reward_growths
}
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Copy)]
pub struct WhirlpoolBumps {
pub whirlpool_bump: u8,
}
#[test]
fn test_whirlpool_reward_info_not_initialized() {
let reward_info = WhirlpoolRewardInfo::default();
assert_eq!(reward_info.initialized(), false);
}
#[test]
fn test_whirlpool_reward_info_initialized() {
let reward_info = &mut WhirlpoolRewardInfo::default();
reward_info.mint = Pubkey::new_unique();
assert_eq!(reward_info.initialized(), true);
}
#[cfg(test)]
pub mod whirlpool_builder {
use super::{Whirlpool, WhirlpoolRewardInfo, NUM_REWARDS};
#[derive(Default)]
pub struct WhirlpoolBuilder {
liquidity: u128,
tick_spacing: u16,
tick_current_index: i32,
sqrt_price: u128,
fee_rate: u16,
protocol_fee_rate: u16,
fee_growth_global_a: u128,
fee_growth_global_b: u128,
reward_last_updated_timestamp: u64,
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
}
impl WhirlpoolBuilder {
pub fn new() -> Self {
Self {
reward_infos: [WhirlpoolRewardInfo::default(); NUM_REWARDS],
..Default::default()
}
}
pub fn liquidity(mut self, liquidity: u128) -> Self {
self.liquidity = liquidity;
self
}
pub fn reward_last_updated_timestamp(mut self, reward_last_updated_timestamp: u64) -> Self {
self.reward_last_updated_timestamp = reward_last_updated_timestamp;
self
}
pub fn reward_info(mut self, index: usize, reward_info: WhirlpoolRewardInfo) -> Self {
self.reward_infos[index] = reward_info;
self
}
pub fn reward_infos(mut self, reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS]) -> Self {
self.reward_infos = reward_infos;
self
}
pub fn tick_spacing(mut self, tick_spacing: u16) -> Self {
self.tick_spacing = tick_spacing;
self
}
pub fn tick_current_index(mut self, tick_current_index: i32) -> Self {
self.tick_current_index = tick_current_index;
self
}
pub fn sqrt_price(mut self, sqrt_price: u128) -> Self {
self.sqrt_price = sqrt_price;
self
}
pub fn fee_growth_global_a(mut self, fee_growth_global_a: u128) -> Self {
self.fee_growth_global_a = fee_growth_global_a;
self
}
pub fn fee_growth_global_b(mut self, fee_growth_global_b: u128) -> Self {
self.fee_growth_global_b = fee_growth_global_b;
self
}
pub fn fee_rate(mut self, fee_rate: u16) -> Self {
self.fee_rate = fee_rate;
self
}
pub fn protocol_fee_rate(mut self, protocol_fee_rate: u16) -> Self {
self.protocol_fee_rate = protocol_fee_rate;
self
}
pub fn build(self) -> Whirlpool {
Whirlpool {
liquidity: self.liquidity,
reward_last_updated_timestamp: self.reward_last_updated_timestamp,
reward_infos: self.reward_infos,
tick_current_index: self.tick_current_index,
sqrt_price: self.sqrt_price,
tick_spacing: self.tick_spacing,
fee_growth_global_a: self.fee_growth_global_a,
fee_growth_global_b: self.fee_growth_global_b,
fee_rate: self.fee_rate,
protocol_fee_rate: self.protocol_fee_rate,
..Default::default()
}
}
}
}

View File

@ -0,0 +1,5 @@
#[cfg(test)]
mod swap_integration_tests;
#[cfg(test)]
pub use swap_integration_tests::*;

View File

@ -0,0 +1,287 @@
use crate::errors::ErrorCode;
use crate::manager::swap_manager::*;
use crate::math::*;
use crate::state::{MAX_TICK_INDEX, MIN_TICK_INDEX, TICK_ARRAY_SIZE};
use crate::util::test_utils::swap_test_fixture::*;
use crate::util::{create_whirlpool_reward_infos, SwapTickSequence};
use serde::Deserialize;
use serde_json;
use serde_with::{serde_as, DisplayFromStr};
use solana_program::msg;
use std::cmp::{max, min};
use std::fs;
#[serde_as]
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct TestCase {
test_id: u16,
description: String,
tick_spacing: u16,
fee_rate: u16,
protocol_fee_rate: u16,
#[serde_as(as = "DisplayFromStr")]
liquidity: u128,
curr_tick_index: i32,
#[serde_as(as = "DisplayFromStr")]
trade_amount: u64,
amount_is_input: bool,
a_to_b: bool,
expectation: Expectation,
}
#[serde_as]
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Expectation {
exception: String,
#[serde_as(as = "DisplayFromStr")]
amount_a: u64,
#[serde_as(as = "DisplayFromStr")]
amount_b: u64,
#[serde_as(as = "DisplayFromStr")]
next_liquidity: u128,
next_tick_index: i32,
#[serde_as(as = "DisplayFromStr")]
next_sqrt_price: u128,
#[serde_as(as = "DisplayFromStr")]
next_fee_growth_global: u128,
#[serde_as(as = "DisplayFromStr")]
next_protocol_fee: u64,
}
/// Current version of Anchor doesn't bubble up errors in a way
/// where we can compare. v0.23.0 has an updated format that will allow us to do so.
const CATCHABLE_ERRORS: [(&str, ErrorCode); 8] = [
(
"MultiplicationShiftRightOverflow",
ErrorCode::MultiplicationShiftRightOverflow,
),
("TokenMaxExceeded", ErrorCode::TokenMaxExceeded),
("DivideByZero", ErrorCode::DivideByZero),
("SqrtPriceOutOfBounds", ErrorCode::SqrtPriceOutOfBounds),
(
"InvalidTickArraySequence",
ErrorCode::InvalidTickArraySequence,
),
("ZeroTradableAmount", ErrorCode::ZeroTradableAmount),
("NumberDownCastError", ErrorCode::NumberDownCastError),
("MultiplicationOverflow", ErrorCode::MultiplicationOverflow),
];
#[test]
/// Run a collection of tests on the swap_manager against expectations
/// A total of 3840 tests on these variables:
/// 1. FeeRate ([MAX_FEE, MAX_PROTOCOL_FEE], [65535, 600], [700, 300], [0, 0])
/// 2. CurrentTickPosition (-443500, -223027, 0, 223027, 443500)
/// 3. Liquidity (0, 2^32, 2^64, 2^110)
/// 4. TickSpacing (1, 8, 128)
/// 5. TradeAmount (0, 10^9, 10^12, U64::max)
/// 6. Trade Direction (a->b, b->a)
/// 7. TradeAmountToken (amountIsInput, amountIsOutput)
fn run_swap_integration_tests() {
let contents =
fs::read_to_string("src/tests/swap_test_cases.json").expect("Failure to read the file.");
let json: Vec<TestCase> = serde_json::from_str(&contents).expect("JSON was not well-formatted");
let test_iterator = json.iter();
let mut total_cases: u16 = 0;
let mut pass_cases: u16 = 0;
let mut fail_cases: u16 = 0;
for test in test_iterator {
let test_id = test.test_id;
total_cases += 1;
let derived_start_tick = derive_start_tick(test.curr_tick_index, test.tick_spacing);
let last_tick_in_seq =
derive_last_tick_in_seq(derived_start_tick, test.tick_spacing, test.a_to_b);
let swap_test_info = SwapTestFixture::new(SwapTestFixtureInfo {
tick_spacing: test.tick_spacing,
liquidity: test.liquidity,
curr_tick_index: test.curr_tick_index,
start_tick_index: derived_start_tick,
trade_amount: test.trade_amount,
sqrt_price_limit: sqrt_price_from_tick_index(last_tick_in_seq),
amount_specified_is_input: test.amount_is_input,
a_to_b: test.a_to_b,
array_1_ticks: &vec![],
array_2_ticks: Some(&vec![]),
array_3_ticks: Some(&vec![]),
fee_growth_global_a: 0,
fee_growth_global_b: 0,
fee_rate: test.fee_rate,
protocol_fee_rate: test.protocol_fee_rate,
reward_infos: create_whirlpool_reward_infos(100, 10),
..Default::default()
});
let mut tick_sequence = SwapTickSequence::new(
swap_test_info.tick_arrays[0].borrow_mut(),
Some(swap_test_info.tick_arrays[1].borrow_mut()),
Some(swap_test_info.tick_arrays[2].borrow_mut()),
);
let post_swap = swap_test_info.eval(&mut tick_sequence, 1643027024);
if post_swap.is_err() {
let e = post_swap.unwrap_err();
if test.expectation.exception.is_empty() {
fail_cases += 1;
msg!("Test case {} - {}", test_id, test.description);
msg!("Received an unexpected error - {}", e.to_string());
msg!("");
continue;
}
let expected_error = derive_error(&test.expectation.exception);
if expected_error.is_none() {
msg!("Test case {} - {}", test_id, test.description);
msg!(
"Expectation expecting an unregistered error - {}. Test received this error - {}",
test.expectation.exception,
e.to_string()
);
msg!("");
fail_cases += 1;
} else if expected_error.is_some() && !expected_error.unwrap().eq(&e) {
fail_cases += 1;
msg!("Test case {} - {}", test_id, test.description);
msg!(
"Test case expected error - {}, but received - {}",
expected_error.unwrap().to_string(),
e.to_string()
);
msg!("");
} else {
pass_cases += 1;
}
} else {
let expectation = &test.expectation;
let results = post_swap.unwrap();
let equal = assert_expectation(&results, expectation);
if equal {
pass_cases += 1;
} else {
msg!("Test case {} - {}", test_id, test.description);
msg!("Fail - results do not equal.");
if !expectation.exception.is_empty() {
msg!(
"Test case received no error but expected error - {}",
expectation.exception
);
} else {
msg!(
"amount_a - {}, expect - {}",
results.amount_a,
expectation.amount_a
);
msg!(
"amount_b - {}, expect - {}",
results.amount_b,
expectation.amount_b
);
msg!(
"next_liq - {}, expect - {}",
results.next_liquidity,
expectation.next_liquidity
);
msg!(
"next_tick - {}, expect - {}",
results.next_tick_index,
expectation.next_tick_index
);
msg!(
"next_sqrt_price - {}, expect - {}",
results.next_sqrt_price,
expectation.next_sqrt_price
);
msg!(
"next_fee_growth_global - {}, expect - {}, delta - {}",
results.next_fee_growth_global,
expectation.next_fee_growth_global,
results.next_fee_growth_global as i128
- expectation.next_fee_growth_global as i128,
);
msg!(
"next_protocol_fee - {}, expect - {}",
results.next_protocol_fee,
expectation.next_protocol_fee
);
}
msg!("");
fail_cases += 1;
}
}
}
msg!(
"Total - {}, Pass - {}, Failed - {}",
total_cases,
pass_cases,
fail_cases
);
assert_eq!(total_cases, pass_cases);
}
fn assert_expectation(post_swap: &PostSwapUpdate, expectation: &Expectation) -> bool {
let amount_a_equal = post_swap.amount_a.eq(&expectation.amount_a);
let amount_b_equal = post_swap.amount_b.eq(&expectation.amount_b);
let next_liquidity_equal = post_swap.next_liquidity.eq(&expectation.next_liquidity);
let next_tick_equal = post_swap.next_tick_index.eq(&expectation.next_tick_index);
let next_sqrt_price_equal = post_swap.next_sqrt_price.eq(&expectation.next_sqrt_price);
let next_fees_equal = post_swap
.next_fee_growth_global
.eq(&expectation.next_fee_growth_global);
let next_protocol_fees_equal = post_swap
.next_protocol_fee
.eq(&expectation.next_protocol_fee);
amount_a_equal
&& amount_b_equal
&& next_liquidity_equal
&& next_tick_equal
&& next_sqrt_price_equal
&& next_fees_equal
&& next_protocol_fees_equal
}
fn derive_error(expected_err: &String) -> Option<ErrorCode> {
for possible_error in CATCHABLE_ERRORS {
if expected_err.eq(&possible_error.0) {
return Some(possible_error.1);
}
}
return None;
}
/// Given a tick & tick-spacing, derive the start tick of the tick-array that this tick would reside in
fn derive_start_tick(curr_tick: i32, tick_spacing: u16) -> i32 {
let num_of_ticks_in_array = TICK_ARRAY_SIZE * tick_spacing as i32;
let rem = curr_tick % num_of_ticks_in_array;
if curr_tick < 0 && rem != 0 {
((curr_tick / num_of_ticks_in_array) - 1) * num_of_ticks_in_array
} else {
curr_tick / num_of_ticks_in_array * num_of_ticks_in_array
}
}
/// Given a start-tick & tick-spacing, derive the last tick of a 3-tick-array sequence
fn derive_last_tick_in_seq(start_tick: i32, tick_spacing: u16, a_to_b: bool) -> i32 {
let num_of_ticks_in_array = TICK_ARRAY_SIZE * tick_spacing as i32;
let potential_last = if a_to_b {
start_tick - (2 * num_of_ticks_in_array)
} else {
start_tick + (3 * num_of_ticks_in_array) - 1
};
max(min(potential_last, MAX_TICK_INDEX), MIN_TICK_INDEX)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
pub mod swap_tick_sequence;
pub mod token;
pub mod util;
pub use swap_tick_sequence::*;
pub use token::*;
pub use util::*;
#[cfg(test)]
pub mod test_utils;
#[cfg(test)]
pub use test_utils::*;

View File

@ -0,0 +1,735 @@
use crate::errors::ErrorCode;
use crate::state::*;
use std::cell::RefMut;
pub struct SwapTickSequence<'info> {
arrays: Vec<RefMut<'info, TickArray>>,
}
impl<'info> SwapTickSequence<'info> {
pub fn new(
ta0: RefMut<'info, TickArray>,
ta1: Option<RefMut<'info, TickArray>>,
ta2: Option<RefMut<'info, TickArray>>,
) -> Self {
let mut vec = Vec::with_capacity(3);
vec.push(ta0);
if ta1.is_some() {
vec.push(ta1.unwrap());
}
if ta2.is_some() {
vec.push(ta2.unwrap());
}
Self { arrays: vec }
}
/// Get the Tick object at the given tick-index & tick-spacing
///
/// # Parameters
/// - `array_index` - the array index that the tick of this given tick-index would be stored in
/// - `tick_index` - the tick index the desired Tick object is stored in
/// - `tick_spacing` - A u8 integer of the tick spacing for this whirlpool
///
/// # Returns
/// - `&Tick`: A reference to the desired Tick object
/// - `TickArrayIndexOutofBounds` - The provided array-index is out of bounds
/// - `TickNotFound`: - The provided tick-index is not an initializable tick index in this Whirlpool w/ this tick-spacing.
pub fn get_tick(
&self,
array_index: usize,
tick_index: i32,
tick_spacing: u16,
) -> Result<&Tick, ErrorCode> {
let array = self.arrays.get(array_index);
match array {
Some(array) => array.get_tick(tick_index, tick_spacing),
_ => Err(ErrorCode::TickArrayIndexOutofBounds),
}
}
/// Updates the Tick object at the given tick-index & tick-spacing
///
/// # Parameters
/// - `array_index` - the array index that the tick of this given tick-index would be stored in
/// - `tick_index` - the tick index the desired Tick object is stored in
/// - `tick_spacing` - A u8 integer of the tick spacing for this whirlpool
/// - `update` - A reference to a TickUpdate object to update the Tick object at the given index
///
/// # Errors
/// - `TickArrayIndexOutofBounds` - The provided array-index is out of bounds
/// - `TickNotFound`: - The provided tick-index is not an initializable tick index in this Whirlpool w/ this tick-spacing.
pub fn update_tick(
&mut self,
array_index: usize,
tick_index: i32,
tick_spacing: u16,
update: &TickUpdate,
) -> Result<(), ErrorCode> {
let array = self.arrays.get_mut(array_index);
match array {
Some(array) => {
array.update_tick(tick_index, tick_spacing, update)?;
Ok(())
}
_ => Err(ErrorCode::TickArrayIndexOutofBounds),
}
}
pub fn get_tick_offset(
&self,
array_index: usize,
tick_index: i32,
tick_spacing: u16,
) -> Result<isize, ErrorCode> {
let array = self.arrays.get(array_index);
match array {
Some(array) => array.tick_offset(tick_index, tick_spacing),
_ => Err(ErrorCode::TickArrayIndexOutofBounds),
}
}
/// Get the next initialized tick in the provided tick range
///
/// # Parameters
/// - `tick_index` - the tick index to start searching from
/// - `tick_spacing` - A u8 integer of the tick spacing for this whirlpool
/// - `a_to_b` - If the trade is from a_to_b, the search will move to the left and the starting search tick is inclusive.
/// If the trade is from b_to_a, the search will move to the right and the starting search tick is not inclusive.
/// - `start_array_index` -
///
/// # Returns
/// - `(usize, i32, &mut Tick)`: The array_index which the next initialized index was found, the next initialized tick-index & a mutable reference to that tick
///
/// - `InvalidTickArraySequence`: - Unable to find the next initialized index with the provided tick-array sequence.
/// - Provided tick-arrays are not in sequential order to the trade direction.
pub fn get_next_initialized_tick_index(
&self,
tick_index: i32,
tick_spacing: u16,
a_to_b: bool,
start_array_index: usize,
) -> Result<(usize, i32), ErrorCode> {
let ticks_in_array = TICK_ARRAY_SIZE * tick_spacing as i32;
let mut search_index = tick_index;
let mut array_index = start_array_index;
// Keep looping the arrays until an initialized tick index in the subsequent tick-arrays found.
loop {
// If we get to the end of the array sequence and next_index is still not found, throw error
let next_array = match self.arrays.get(array_index) {
Some(array) => array,
None => return Err(ErrorCode::TickArraySequenceInvalidIndex),
};
let next_index =
next_array.get_next_init_tick_index(search_index, tick_spacing, a_to_b)?;
match next_index {
Some(next_index) => {
return Ok((array_index, next_index));
}
None => {
// If we are at the last valid tick array, return the min/max tick index
if a_to_b && next_array.is_min_tick_array() {
return Ok((array_index, MIN_TICK_INDEX));
} else if !a_to_b && next_array.is_max_tick_array(tick_spacing) {
return Ok((array_index, MAX_TICK_INDEX));
}
// If we are at the last tick array in the sequencer, return the last tick
if array_index + 1 == self.arrays.len() {
if a_to_b {
return Ok((array_index, next_array.start_tick_index));
} else {
let last_tick = next_array.start_tick_index + ticks_in_array - 1;
return Ok((array_index, last_tick));
}
}
// No initialized index found. Move the search-index to the 1st search position
// of the next array in sequence.
search_index = if a_to_b {
next_array.start_tick_index - 1
} else {
next_array.start_tick_index + ticks_in_array - 1
};
array_index += 1;
}
}
}
}
}
#[cfg(test)]
mod swap_tick_sequence_tests {
use super::*;
use std::cell::RefCell;
const TS_8: u16 = 8;
const TS_128: u16 = 128;
const LAST_TICK_OFFSET: usize = TICK_ARRAY_SIZE as usize - 1;
fn build_tick_array(
start_tick_index: i32,
initialized_offsets: Vec<usize>,
) -> RefCell<TickArray> {
let mut array = TickArray::default();
array.start_tick_index = start_tick_index;
for offset in initialized_offsets {
let mut new_tick = Tick::default();
new_tick.initialized = true;
array.ticks[offset] = new_tick;
}
RefCell::new(array)
}
mod modify_ticks {
use super::*;
#[test]
fn modify_tick_init_tick() {
let ta0 = build_tick_array(11264, vec![50]);
let ta1 = build_tick_array(0, vec![25, 71]);
let ta2 = build_tick_array(-11264, vec![25, 35, 56]);
let mut swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let initialized_ticks_offsets = [(0, 50), (1, 25), (1, 71), (2, 25), (2, 35), (2, 56)];
for init_tick_offset in initialized_ticks_offsets {
let array_index = init_tick_offset.0 as usize;
let tick_index = 11264 - array_index as i32 * TS_128 as i32 * TICK_ARRAY_SIZE
+ init_tick_offset.1 * TS_128 as i32;
let result = swap_tick_sequence.get_tick(array_index, tick_index, TS_128);
assert_eq!(result.is_ok(), true);
assert_eq!(result.unwrap().initialized, true);
let update_result = swap_tick_sequence.update_tick(
array_index,
tick_index,
TS_128,
&TickUpdate {
initialized: false,
liquidity_net: 1500,
..Default::default()
},
);
assert_eq!(update_result.is_ok(), true);
let get_updated_result = swap_tick_sequence
.get_tick(array_index, tick_index, TS_128)
.unwrap();
let liq_net = get_updated_result.liquidity_net;
assert_eq!(liq_net, 1500);
}
}
#[test]
fn modify_tick_uninitializable_tick() {
let ta0 = build_tick_array(9216, vec![50]);
let ta1 = build_tick_array(0, vec![25, 71]);
let ta2 = build_tick_array(-9216, vec![25, 35, 56]);
let mut swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let uninitializable_tick_indices = [(0, 9217), (1, 257), (2, -5341)];
for uninitializable_search_tick in uninitializable_tick_indices {
let result = swap_tick_sequence.get_tick(
uninitializable_search_tick.0,
uninitializable_search_tick.1,
TS_128,
);
assert_eq!(result.unwrap_err(), ErrorCode::TickNotFound);
let update_result = swap_tick_sequence.update_tick(
uninitializable_search_tick.0,
uninitializable_search_tick.1,
TS_128,
&TickUpdate {
initialized: false,
liquidity_net: 1500,
..Default::default()
},
);
assert_eq!(update_result.unwrap_err(), ErrorCode::TickNotFound);
}
}
#[test]
fn modify_tick_uninitialized_tick() {
let ta0 = build_tick_array(9216, vec![50]);
let ta1 = build_tick_array(0, vec![25, 71]);
let ta2 = build_tick_array(-9216, vec![25, 35, 56]);
let mut swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let uninitialized_tick_indices = [(0, 13696), (1, 0), (1, 3072), (2, -3456)];
for uninitializable_search_tick in uninitialized_tick_indices {
let result = swap_tick_sequence.get_tick(
uninitializable_search_tick.0,
uninitializable_search_tick.1,
TS_128,
);
assert_eq!(result.unwrap().initialized, false);
let update_result = swap_tick_sequence.update_tick(
uninitializable_search_tick.0,
uninitializable_search_tick.1,
TS_128,
&TickUpdate {
initialized: true,
liquidity_net: 1500,
..Default::default()
},
);
assert_eq!(update_result.is_ok(), true);
let get_updated_result = swap_tick_sequence
.get_tick(
uninitializable_search_tick.0,
uninitializable_search_tick.1,
TS_128,
)
.unwrap();
assert_eq!(get_updated_result.initialized, true);
let liq_net = get_updated_result.liquidity_net;
assert_eq!(liq_net, 1500);
}
}
#[test]
fn cannot_modify_invalid_array_index() {
let ta0 = build_tick_array(9216, vec![50]);
let ta1 = build_tick_array(0, vec![25, 71]);
let ta2 = build_tick_array(-9216, vec![25, 35, 56]);
let mut swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let get_result = swap_tick_sequence.get_tick(3, 5000, TS_128);
assert_eq!(
get_result.unwrap_err(),
ErrorCode::TickArrayIndexOutofBounds
);
let update_result = swap_tick_sequence.update_tick(
3,
5000,
TS_128,
&TickUpdate {
..Default::default()
},
);
assert_eq!(
update_result.unwrap_err(),
ErrorCode::TickArrayIndexOutofBounds
);
}
}
mod a_to_b {
use super::*;
#[test]
/// In an a_to_b search, the search-range of a tick-array is between 0 & last-tick - 1
fn search_range() {
let ta0 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let ta1 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let ta2 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
// Verify start range is ok at start-tick-index
let (start_range_array_index, start_range_result_index) = swap_tick_sequence
.get_next_initialized_tick_index(0, TS_128, true, 2)
.unwrap();
assert_eq!(start_range_result_index, 0);
assert_eq!(start_range_array_index, 2);
// Verify search is ok at the last tick-index in array
let last_tick_in_array = (TICK_ARRAY_SIZE as i32 * TS_128 as i32) - 1;
let expected_last_usable_tick_index = LAST_TICK_OFFSET as i32 * TS_128 as i32;
let (end_range_array_index, end_range_result_index) = swap_tick_sequence
.get_next_initialized_tick_index(last_tick_in_array, TS_128, true, 2)
.unwrap();
assert_eq!(end_range_result_index, expected_last_usable_tick_index);
assert_eq!(end_range_array_index, 2);
}
#[test]
/// On a b_to_a search where the search_index is within [-tickSpacing, 0) and search array begins at 0, correctly
/// uses 0 as next initialized. This test is shifted by TICK_ARRAY_SIZE * TS_128.
fn search_range_on_left() {
let ta0 = build_tick_array(
TICK_ARRAY_SIZE * TS_128 as i32,
vec![0, 1, LAST_TICK_OFFSET],
);
let swap_tick_sequence = SwapTickSequence::new(ta0.borrow_mut(), None, None);
// Verify start range is ok at start-tick-index
let (start_range_array_index, start_range_result_index) = swap_tick_sequence
.get_next_initialized_tick_index(
TICK_ARRAY_SIZE * (TS_128 as i32) - 40,
TS_128,
false,
0,
)
.unwrap();
assert_eq!(start_range_array_index, 0);
assert_eq!(start_range_result_index, TICK_ARRAY_SIZE * TS_128 as i32);
}
#[test]
#[should_panic(expected = "InvalidTickArraySequence")]
/// In an a_to_b search, search will panic if search index is on the last tick in array + 1
fn range_panic_on_end_range_plus_one() {
let ta0 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let ta1 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let ta2 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let last_tick_in_array_plus_one = TICK_ARRAY_SIZE as i32 * TS_8 as i32;
let (_, _) = swap_tick_sequence
.get_next_initialized_tick_index(last_tick_in_array_plus_one, TS_8, true, 1)
.unwrap();
}
#[test]
#[should_panic(expected = "InvalidTickArraySequence")]
/// In an a_to_b search, search will panic if search index is on the first tick in array - 1
fn range_panic_on_start_range_sub_one() {
let ta0 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let ta1 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let ta2 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let (_, _) = swap_tick_sequence
.get_next_initialized_tick_index(-1, TS_8, true, 2)
.unwrap();
}
}
mod b_to_a {
use super::*;
#[test]
/// In an b_to_a search, the search-range of a tick-array is between the last usable tick in the last array
/// & the last usable tick in this array minus one.
fn search_range() {
let ta0 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let swap_tick_sequence = SwapTickSequence::new(ta0.borrow_mut(), None, None);
// Verify start range is ok at start-tick-index
let (start_range_array_index, start_range_result_index) = swap_tick_sequence
.get_next_initialized_tick_index(TS_8 as i32 * -1, TS_8, false, 0)
.unwrap();
assert_eq!(start_range_result_index, 0);
assert_eq!(start_range_array_index, 0);
// Verify search is ok at the last tick-index in array
let last_searchable_tick_in_array = LAST_TICK_OFFSET as i32 * TS_8 as i32 - 1;
let last_usable_tick_in_array = LAST_TICK_OFFSET as i32 * TS_8 as i32;
let (end_range_array_index, end_range_result_index) = swap_tick_sequence
.get_next_initialized_tick_index(last_searchable_tick_in_array, TS_8, false, 0)
.unwrap();
assert_eq!(end_range_result_index, last_usable_tick_in_array);
assert_eq!(end_range_array_index, 0);
}
#[test]
#[should_panic(expected = "InvalidTickArraySequence")]
/// In an b_to_a search, search will panic if search index is on the last usable tick
fn range_panic_on_end_range_plus_one() {
let ta0 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let swap_tick_sequence = SwapTickSequence::new(ta0.borrow_mut(), None, None);
let last_searchable_tick_in_array_plus_one = LAST_TICK_OFFSET as i32 * TS_8 as i32;
let (_, _) = swap_tick_sequence
.get_next_initialized_tick_index(
last_searchable_tick_in_array_plus_one,
TS_8,
false,
0,
)
.unwrap();
}
#[test]
#[should_panic(expected = "InvalidTickArraySequence")]
/// In an b_to_a search, search will panic if search index is less than the last usable tick in the previous tick-array
fn range_panic_on_start_range_sub_one() {
let ta0 = build_tick_array(0, vec![0, LAST_TICK_OFFSET]);
let swap_tick_sequence = SwapTickSequence::new(ta0.borrow_mut(), None, None);
let (_, _) = swap_tick_sequence
.get_next_initialized_tick_index(TS_8 as i32 * -1 - 1, TS_8, false, 0)
.unwrap();
}
}
mod tick_bound {
use super::*;
/// SwapTickSequence will bound the ticks by tick-array, not max/min tick. This is to reduce duplicated responsibility
/// between thsi & the swap loop / compute_swap.
#[test]
fn b_to_a_search_reaching_max_tick() {
let ta0 = build_tick_array(0, vec![]);
let ta1 = build_tick_array(0, vec![]);
let ta2 = build_tick_array(443520, vec![]); // Max(443636).div_floor(tick-spacing (8) * TA Size (72))* tick-spacing (8) * TA Size (72)
let swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let (array_index, index) = swap_tick_sequence
.get_next_initialized_tick_index(443521, TS_8, false, 2)
.unwrap();
assert_eq!(index, 443636);
assert_eq!(array_index, 2);
}
#[test]
fn a_to_b_search_reaching_min_tick() {
let ta0 = build_tick_array(0, vec![]);
let ta1 = build_tick_array(0, vec![]);
let ta2 = build_tick_array(-444096, vec![]); // Min(-443636).div_ceil(tick-spacing (8) * TA Size (72)) * tick-spacing (8) * TA Size (72)
let swap_tick_sequence = SwapTickSequence::new(
ta2.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta0.borrow_mut()),
);
let (array_index, index) = swap_tick_sequence
.get_next_initialized_tick_index(-443521, TS_8, true, 0)
.unwrap();
assert_eq!(index, -443636);
assert_eq!(array_index, 0);
}
}
#[test]
/// Search index on an initialized tick index will return that tick in a a_to_b search
/// Expect:
/// - The same tick will be returned if search index is an initialized tick
fn a_to_b_search_on_initialized_index() {
let ta0 = build_tick_array(9216, vec![]);
let ta1 = build_tick_array(0, vec![25, 71]);
let ta2 = build_tick_array(-9216, vec![25, 35, 56]);
let swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let (array_index, index) = swap_tick_sequence
.get_next_initialized_tick_index(9088, TS_128, true, 1)
.unwrap();
assert_eq!(index, 9088);
assert_eq!(array_index, 1);
let tick = swap_tick_sequence
.get_tick(array_index, index, TS_128)
.unwrap();
assert_eq!(tick.initialized, true);
}
#[test]
/// a-to-b search through the entire tick-array sequence
///
/// Verifies:
/// - Search index will not return previous initialized indicies in a b_to_a search
/// - If the search reaches the end of the last tick array, return the first tick index of the last tick array
fn a_to_b_search_entire_range() {
let ta0 = build_tick_array(9216, vec![]);
let ta1 = build_tick_array(0, vec![25, 71]);
let ta2 = build_tick_array(-9216, vec![25, 35, 56]);
let swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let mut search_index = 18431;
let mut curr_array_index = 0;
let expectation = [
(9088, 1, true),
(3200, 1, true),
(-2048, 2, true),
(-4736, 2, true),
(-6016, 2, true),
(-9216, 2, false),
];
for i in 0..expectation.len() {
let (array_index, index) = swap_tick_sequence
.get_next_initialized_tick_index(search_index, TS_128, true, curr_array_index)
.unwrap();
assert_eq!(index, expectation[i].0);
assert_eq!(array_index, expectation[i].1);
let tick = swap_tick_sequence
.get_tick(array_index, index, TS_128)
.unwrap();
assert_eq!(tick.initialized, expectation[i].2);
// users on a_to_b search must manually decrement since a_to_b is inclusive of current-tick
search_index = index - 1;
curr_array_index = array_index;
}
}
#[test]
/// b-to-a search through the entire tick-array sequence
///
/// Verifies:
/// - Search index on an initialized tick index will not return that tick in a b_to_a search
/// - Search index will not return previous initialized indicies in a b_to_a search
/// - Search indicies within the shifted range (-tick-spacing prior to the start-tick) will
/// return a valid initialized tick
/// - If the search reaches the last tick array, return the last tick in the last tick array
fn b_to_a_search_entire_range() {
let ta0 = build_tick_array(0, vec![10, 25]);
let ta1 = build_tick_array(704, vec![]);
let ta2 = build_tick_array(1408, vec![10, 50, 25]);
let swap_tick_sequence = SwapTickSequence::new(
ta0.borrow_mut(),
Some(ta1.borrow_mut()),
Some(ta2.borrow_mut()),
);
let mut search_index = -7;
let mut curr_array_index = 0;
let expectation = [
(80, 0, true),
(200, 0, true),
(1488, 2, true),
(1608, 2, true),
(1808, 2, true),
(2111, 2, false),
];
for i in 0..expectation.len() {
let (array_index, index) = swap_tick_sequence
.get_next_initialized_tick_index(search_index, TS_8, false, curr_array_index)
.unwrap();
assert_eq!(index, expectation[i].0);
assert_eq!(array_index, expectation[i].1);
let mut tick_initialized = false;
if Tick::check_is_usable_tick(index, TS_8) {
tick_initialized = swap_tick_sequence
.get_tick(array_index, index, TS_8)
.unwrap()
.initialized;
};
assert_eq!(tick_initialized, expectation[i].2);
search_index = index;
curr_array_index = array_index;
}
}
#[test]
#[should_panic(expected = "InvalidTickArraySequence")]
/// The starting point of a swap should always be contained within the first array
/// Expected:
/// - Panic on InvalidTickArraySequence on 1st array
fn array_0_out_of_sequence() {
let ta0 = build_tick_array(0, vec![10, 25]);
let ta1 = build_tick_array(720, vec![53, 71]);
let ta2 = build_tick_array(1440, vec![10, 50, 25]);
let swap_tick_sequence = SwapTickSequence::new(
ta1.borrow_mut(),
Some(ta0.borrow_mut()),
Some(ta2.borrow_mut()),
);
let mut search_index = -5;
let mut curr_array_index = 0;
for _ in 0..10 {
let (array_index, index) = swap_tick_sequence
.get_next_initialized_tick_index(search_index, TS_8, false, curr_array_index)
.unwrap();
search_index = index;
curr_array_index = array_index;
}
}
#[test]
#[should_panic(expected = "InvalidTickArraySequence")]
/// Search sequence will be successful up until invalid tick array sequence
///
/// Expected:
/// - Does not panic when traversing tick-array 0
/// - Panic on InvalidTickArraySequence when search-sequence is not in array 1's range
fn array_1_out_of_sequence() {
let ta0 = build_tick_array(-576, vec![10]);
let ta1 = build_tick_array(0, vec![10]);
let ta2 = build_tick_array(576, vec![25]);
let swap_tick_sequence = SwapTickSequence::new(
ta2.borrow_mut(),
Some(ta0.borrow_mut()),
Some(ta1.borrow_mut()),
);
let mut search_index = 1439;
let mut curr_array_index = 0;
let expectation = [(776, 0, true), (576, 0, false), (80, 0, true)];
for i in 0..expectation.len() {
let (array_index, index) = swap_tick_sequence
.get_next_initialized_tick_index(search_index, TS_8, true, curr_array_index)
.unwrap();
assert_eq!(index, expectation[i].0);
assert_eq!(array_index, expectation[i].1);
let tick = swap_tick_sequence
.get_tick(array_index, index, TS_8)
.unwrap();
assert_eq!(tick.initialized, expectation[i].2);
search_index = index - 1;
curr_array_index = array_index;
}
}
}

View File

@ -0,0 +1,254 @@
use crate::manager::liquidity_manager::ModifyLiquidityUpdate;
use crate::manager::tick_manager::next_tick_cross_update;
use crate::manager::whirlpool_manager::*;
use crate::math::{add_liquidity_delta, Q64_RESOLUTION};
use crate::state::position_builder::PositionBuilder;
use crate::state::{
tick::*, tick_builder::TickBuilder, whirlpool_builder::WhirlpoolBuilder, Whirlpool,
};
use crate::state::{
Position, PositionRewardInfo, PositionUpdate, WhirlpoolRewardInfo, NUM_REWARDS,
};
use anchor_lang::prelude::*;
const BELOW_LOWER_TICK_INDEX: i32 = -120;
const ABOVE_UPPER_TICK_INDEX: i32 = 120;
pub enum CurrIndex {
Below,
Inside,
Above,
}
pub enum TickLabel {
Upper,
Lower,
}
pub enum Direction {
Left,
Right,
}
// State for testing modifying liquidity in a single whirlpool position
pub struct LiquidityTestFixture {
pub whirlpool: Whirlpool,
pub position: Position,
pub tick_lower: Tick,
pub tick_upper: Tick,
}
pub struct LiquidityTestFixtureInfo {
pub curr_index_loc: CurrIndex,
pub whirlpool_liquidity: u128,
pub position_liquidity: u128,
pub tick_lower_liquidity_gross: u128,
pub tick_upper_liquidity_gross: u128,
pub fee_growth_global_a: u128,
pub fee_growth_global_b: u128,
pub reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
}
impl LiquidityTestFixture {
pub fn new(info: LiquidityTestFixtureInfo) -> LiquidityTestFixture {
assert!(info.tick_lower_liquidity_gross < i64::MAX as u128);
assert!(info.tick_upper_liquidity_gross < i64::MAX as u128);
// Tick's must have enough at least enough liquidity to support the position
assert!(info.tick_lower_liquidity_gross >= info.position_liquidity);
assert!(info.tick_upper_liquidity_gross >= info.position_liquidity);
let curr_index = match info.curr_index_loc {
CurrIndex::Below => BELOW_LOWER_TICK_INDEX,
CurrIndex::Inside => 0,
CurrIndex::Above => ABOVE_UPPER_TICK_INDEX,
};
let whirlpool = WhirlpoolBuilder::new()
.tick_current_index(curr_index)
.liquidity(info.whirlpool_liquidity)
.reward_infos(info.reward_infos)
.fee_growth_global_a(info.fee_growth_global_a)
.fee_growth_global_b(info.fee_growth_global_b)
.build();
let tick_lower_initialized = info.tick_lower_liquidity_gross > 0;
let tick_upper_initialized = info.tick_upper_liquidity_gross > 0;
LiquidityTestFixture {
whirlpool,
position: PositionBuilder::new(-100, 100)
.liquidity(info.position_liquidity)
.build(),
tick_lower: TickBuilder::default()
.initialized(tick_lower_initialized)
.liquidity_gross(info.tick_lower_liquidity_gross)
.liquidity_net(info.tick_lower_liquidity_gross as i128)
.build(),
tick_upper: TickBuilder::default()
.initialized(tick_upper_initialized)
.liquidity_gross(info.tick_upper_liquidity_gross)
.liquidity_net(-(info.tick_upper_liquidity_gross as i128))
.build(),
}
}
pub fn increment_whirlpool_fee_growths(
&mut self,
fee_growth_delta_a: u128,
fee_growth_delta_b: u128,
) {
self.whirlpool.fee_growth_global_a = self
.whirlpool
.fee_growth_global_a
.wrapping_add(fee_growth_delta_a);
self.whirlpool.fee_growth_global_b = self
.whirlpool
.fee_growth_global_b
.wrapping_add(fee_growth_delta_b);
}
pub fn increment_whirlpool_reward_growths_by_time(&mut self, seconds: u64) {
let next_timestamp = self.whirlpool.reward_last_updated_timestamp + seconds;
self.whirlpool.reward_infos =
next_whirlpool_reward_infos(&self.whirlpool, next_timestamp).unwrap();
self.whirlpool.reward_last_updated_timestamp = next_timestamp;
}
/// Simulates crossing a tick within the test fixture.
pub fn cross_tick(&mut self, tick_label: TickLabel, direction: Direction) {
let tick = match tick_label {
TickLabel::Lower => &mut self.tick_lower,
TickLabel::Upper => &mut self.tick_upper,
};
let update = next_tick_cross_update(
tick,
self.whirlpool.fee_growth_global_a,
self.whirlpool.fee_growth_global_b,
&self.whirlpool.reward_infos,
)
.unwrap();
tick.update(&update);
self.whirlpool.liquidity = add_liquidity_delta(
self.whirlpool.liquidity,
match direction {
Direction::Left => -tick.liquidity_net,
Direction::Right => tick.liquidity_net,
},
)
.unwrap();
match tick_label {
TickLabel::Lower => match direction {
Direction::Right => self.whirlpool.tick_current_index = 0,
Direction::Left => self.whirlpool.tick_current_index = BELOW_LOWER_TICK_INDEX,
},
TickLabel::Upper => match direction {
Direction::Left => self.whirlpool.tick_current_index = 0,
Direction::Right => self.whirlpool.tick_current_index = ABOVE_UPPER_TICK_INDEX,
},
}
}
pub fn apply_update(
&mut self,
update: &ModifyLiquidityUpdate,
reward_last_updated_timestamp: u64,
) {
assert!(reward_last_updated_timestamp >= self.whirlpool.reward_last_updated_timestamp);
self.whirlpool.reward_last_updated_timestamp = reward_last_updated_timestamp;
self.whirlpool.liquidity = update.whirlpool_liquidity;
self.whirlpool.reward_infos = update.reward_infos;
self.tick_lower.update(&update.tick_lower_update);
self.tick_upper.update(&update.tick_upper_update);
self.position.update(&update.position_update);
}
}
pub fn create_whirlpool_reward_infos(
emissions_per_second_x64: u128,
growth_global_x64: u128,
) -> [WhirlpoolRewardInfo; NUM_REWARDS] {
[
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64,
growth_global_x64,
..Default::default()
},
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64,
growth_global_x64,
..Default::default()
},
WhirlpoolRewardInfo {
mint: Pubkey::new_unique(),
emissions_per_second_x64,
growth_global_x64,
..Default::default()
},
]
}
pub fn create_position_reward_infos(
growth_inside_checkpoint: u128,
amount_owed: u64,
) -> [PositionRewardInfo; NUM_REWARDS] {
[
PositionRewardInfo {
growth_inside_checkpoint,
amount_owed,
},
PositionRewardInfo {
growth_inside_checkpoint,
amount_owed,
},
PositionRewardInfo {
growth_inside_checkpoint,
amount_owed,
},
]
}
pub fn create_reward_growths(growth_global_x64: u128) -> [u128; NUM_REWARDS] {
[growth_global_x64, growth_global_x64, growth_global_x64]
}
pub fn to_x64(n: u128) -> u128 {
n << Q64_RESOLUTION
}
pub fn assert_whirlpool_reward_growths(
reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS],
expected_growth: u128,
) {
assert_eq!(
WhirlpoolRewardInfo::to_reward_growths(reward_infos),
create_reward_growths(expected_growth)
)
}
pub struct ModifyLiquidityExpectation {
pub whirlpool_liquidity: u128,
pub whirlpool_reward_growths: [u128; NUM_REWARDS],
pub position_update: PositionUpdate,
pub tick_lower_update: TickUpdate,
pub tick_upper_update: TickUpdate,
}
pub fn assert_modify_liquidity(
update: &ModifyLiquidityUpdate,
expect: &ModifyLiquidityExpectation,
) {
assert_eq!(update.whirlpool_liquidity, expect.whirlpool_liquidity);
assert_eq!(
WhirlpoolRewardInfo::to_reward_growths(&update.reward_infos),
expect.whirlpool_reward_growths
);
assert_eq!(update.tick_lower_update, expect.tick_lower_update);
assert_eq!(update.tick_upper_update, expect.tick_upper_update);
assert_eq!(update.position_update, expect.position_update);
}

View File

@ -0,0 +1,5 @@
pub mod liquidity_test_fixture;
pub mod swap_test_fixture;
pub use liquidity_test_fixture::*;
pub use swap_test_fixture::*;

View File

@ -0,0 +1,236 @@
use crate::errors::ErrorCode;
use crate::manager::swap_manager::*;
use crate::math::tick_math::*;
use crate::state::{
tick::*, tick_builder::TickBuilder, whirlpool_builder::WhirlpoolBuilder, TickArray, Whirlpool,
};
use crate::state::{WhirlpoolRewardInfo, NUM_REWARDS};
use crate::util::SwapTickSequence;
use anchor_lang::prelude::*;
use std::cell::RefCell;
pub const TS_8: u16 = 8;
pub const TS_128: u16 = 128;
const NO_TICKS_VEC: &Vec<TestTickInfo> = &vec![];
pub struct SwapTestFixture {
pub whirlpool: Whirlpool,
pub tick_arrays: Vec<RefCell<TickArray>>,
pub trade_amount: u64,
pub sqrt_price_limit: u128,
pub amount_specified_is_input: bool,
pub a_to_b: bool,
pub reward_last_updated_timestamp: u64,
}
#[derive(Default)]
pub struct TestTickInfo {
pub index: i32,
pub liquidity_net: i128,
pub fee_growth_outside_a: u128,
pub fee_growth_outside_b: u128,
pub reward_growths_outside: [u128; NUM_REWARDS],
}
pub struct SwapTestFixtureInfo<'info> {
pub tick_spacing: u16,
pub liquidity: u128,
pub curr_tick_index: i32,
pub start_tick_index: i32,
pub trade_amount: u64,
pub sqrt_price_limit: u128,
pub amount_specified_is_input: bool,
pub a_to_b: bool,
pub reward_last_updated_timestamp: u64,
pub reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
pub fee_growth_global_a: u128,
pub fee_growth_global_b: u128,
pub array_1_ticks: &'info Vec<TestTickInfo>,
pub array_2_ticks: Option<&'info Vec<TestTickInfo>>,
pub array_3_ticks: Option<&'info Vec<TestTickInfo>>,
pub fee_rate: u16,
pub protocol_fee_rate: u16,
}
impl<'info> Default for SwapTestFixtureInfo<'info> {
fn default() -> Self {
SwapTestFixtureInfo {
tick_spacing: TS_128,
liquidity: 0,
curr_tick_index: 0,
start_tick_index: 0,
trade_amount: 0,
sqrt_price_limit: 0,
amount_specified_is_input: false,
a_to_b: false,
reward_last_updated_timestamp: 0,
reward_infos: [
WhirlpoolRewardInfo::default(),
WhirlpoolRewardInfo::default(),
WhirlpoolRewardInfo::default(),
],
fee_growth_global_a: 0,
fee_growth_global_b: 0,
array_1_ticks: &NO_TICKS_VEC,
array_2_ticks: None,
array_3_ticks: None,
fee_rate: 0,
protocol_fee_rate: 0,
}
}
}
pub struct SwapTestExpectation {
pub traded_amount_a: u64,
pub traded_amount_b: u64,
pub end_tick_index: i32,
pub end_liquidity: u128,
pub end_reward_growths: [u128; NUM_REWARDS],
}
#[derive(Default)]
pub struct TickExpectation {
pub fee_growth_outside_a: u128,
pub fee_growth_outside_b: u128,
pub reward_growths_outside: [u128; NUM_REWARDS],
}
pub fn assert_swap(swap_update: &PostSwapUpdate, expect: &SwapTestExpectation) {
assert_eq!(swap_update.amount_a, expect.traded_amount_a);
assert_eq!(swap_update.amount_b, expect.traded_amount_b);
assert_eq!(swap_update.next_tick_index, expect.end_tick_index);
assert_eq!(swap_update.next_liquidity, expect.end_liquidity);
assert_eq!(
WhirlpoolRewardInfo::to_reward_growths(&swap_update.next_reward_infos),
expect.end_reward_growths
);
}
pub fn assert_swap_tick_state(tick: &Tick, expect: &TickExpectation) {
assert_eq!({ tick.fee_growth_outside_a }, expect.fee_growth_outside_a);
assert_eq!({ tick.fee_growth_outside_b }, expect.fee_growth_outside_b);
assert_eq!(
{ tick.reward_growths_outside },
expect.reward_growths_outside
);
}
pub fn build_filled_tick_array(start_index: i32, tick_spacing: u16) -> Vec<TestTickInfo> {
let mut array_ticks: Vec<TestTickInfo> = vec![];
for n in 0..TICK_ARRAY_SIZE {
let index = start_index + n * tick_spacing as i32;
if index >= MIN_TICK_INDEX && index < MAX_TICK_INDEX {
array_ticks.push(TestTickInfo {
index,
liquidity_net: -5,
..Default::default()
});
}
}
array_ticks
}
impl SwapTestFixture {
pub fn new<'info>(info: SwapTestFixtureInfo) -> SwapTestFixture {
let whirlpool = WhirlpoolBuilder::new()
.liquidity(info.liquidity)
.sqrt_price(sqrt_price_from_tick_index(info.curr_tick_index))
.tick_spacing(info.tick_spacing)
.tick_current_index(info.curr_tick_index)
.reward_last_updated_timestamp(info.reward_last_updated_timestamp)
.reward_infos(info.reward_infos)
.fee_growth_global_a(info.fee_growth_global_a)
.fee_growth_global_b(info.fee_growth_global_b)
.fee_rate(info.fee_rate)
.protocol_fee_rate(info.protocol_fee_rate)
.build();
let array_ticks: Vec<Option<&Vec<TestTickInfo>>> = vec![
Some(&info.array_1_ticks),
info.array_2_ticks,
info.array_3_ticks,
];
let mut ref_mut_tick_arrays = Vec::with_capacity(3);
let direction: i32 = if info.a_to_b { -1 } else { 1 };
let mut array_index = 0;
for array in array_ticks.iter() {
let array_start_tick_index = info.start_tick_index
+ info.tick_spacing as i32 * TICK_ARRAY_SIZE * array_index * direction;
array_index += 1;
let mut new_ta = TickArray {
start_tick_index: array_start_tick_index,
ticks: [Tick::default(); TICK_ARRAY_SIZE_USIZE],
whirlpool: Pubkey::default(),
};
if array.is_none() {
ref_mut_tick_arrays.push(RefCell::new(new_ta));
continue;
}
let tick_array = array.unwrap();
for tick in tick_array {
let update = TickUpdate::from(
&TickBuilder::default()
.initialized(true)
.liquidity_net(tick.liquidity_net)
.fee_growth_outside_a(tick.fee_growth_outside_a)
.fee_growth_outside_b(tick.fee_growth_outside_b)
.reward_growths_outside(tick.reward_growths_outside)
.build(),
);
let update_result = new_ta.update_tick(tick.index, info.tick_spacing, &update);
if update_result.is_err() {
panic!("Failed to set tick {}", tick.index);
}
}
ref_mut_tick_arrays.push(RefCell::new(new_ta));
}
SwapTestFixture {
whirlpool,
tick_arrays: ref_mut_tick_arrays,
trade_amount: info.trade_amount,
sqrt_price_limit: info.sqrt_price_limit,
amount_specified_is_input: info.amount_specified_is_input,
a_to_b: info.a_to_b,
reward_last_updated_timestamp: info.reward_last_updated_timestamp,
}
}
pub fn run(&self, tick_sequence: &mut SwapTickSequence, next_timestamp: u64) -> PostSwapUpdate {
swap(
&self.whirlpool,
tick_sequence,
self.trade_amount,
self.sqrt_price_limit,
self.amount_specified_is_input,
self.a_to_b,
next_timestamp,
)
.unwrap()
}
pub fn eval(
&self,
tick_sequence: &mut SwapTickSequence,
next_timestamp: u64,
) -> Result<PostSwapUpdate, ErrorCode> {
swap(
&self.whirlpool,
tick_sequence,
self.trade_amount,
self.sqrt_price_limit,
self.amount_specified_is_input,
self.a_to_b,
next_timestamp,
)
}
}

View File

@ -0,0 +1,214 @@
use crate::state::Whirlpool;
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};
use mpl_token_metadata::instruction::create_metadata_accounts_v2;
use solana_program::program::invoke_signed;
use spl_token::instruction::{burn_checked, close_account, mint_to, set_authority, AuthorityType};
pub fn transfer_from_owner_to_vault<'info>(
position_authority: &Signer<'info>,
token_owner_account: &Account<'info, TokenAccount>,
token_vault: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
amount: u64,
) -> Result<(), ProgramError> {
token::transfer(
CpiContext::new(
token_program.to_account_info().clone(),
Transfer {
from: token_owner_account.to_account_info().clone(),
to: token_vault.to_account_info().clone(),
authority: position_authority.to_account_info().clone(),
},
),
amount,
)
}
pub fn transfer_from_vault_to_owner<'info>(
whirlpool: &Account<'info, Whirlpool>,
token_vault: &Account<'info, TokenAccount>,
token_owner_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
amount: u64,
) -> Result<(), ProgramError> {
token::transfer(
CpiContext::new_with_signer(
token_program.to_account_info().clone(),
Transfer {
from: token_vault.to_account_info().clone(),
to: token_owner_account.to_account_info().clone(),
authority: whirlpool.to_account_info().clone(),
},
&[&whirlpool.seeds()],
),
amount,
)
}
pub fn burn_and_close_user_position_token<'info>(
token_authority: &Signer<'info>,
receiver: &UncheckedAccount<'info>,
position_mint: &Account<'info, Mint>,
position_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
) -> ProgramResult {
// Burn a single token in user account
invoke_signed(
&burn_checked(
token_program.key,
position_token_account.to_account_info().key,
position_mint.to_account_info().key,
token_authority.key,
&[],
1,
position_mint.decimals,
)?,
&[
token_program.to_account_info().clone(),
position_token_account.to_account_info().clone(),
position_mint.to_account_info().clone(),
token_authority.to_account_info().clone(),
],
&[],
)?;
// Close user account
invoke_signed(
&close_account(
token_program.key,
position_token_account.to_account_info().key,
receiver.key,
token_authority.key,
&[],
)?,
&[
token_program.to_account_info().clone(),
position_token_account.to_account_info().clone(),
receiver.to_account_info().clone(),
token_authority.to_account_info().clone(),
],
&[],
)
}
pub fn mint_position_token_and_remove_authority<'info>(
whirlpool: &Account<'info, Whirlpool>,
position_mint: &Account<'info, Mint>,
position_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
) -> ProgramResult {
mint_position_token(
whirlpool,
position_mint,
position_token_account,
token_program,
)?;
remove_position_token_mint_authority(whirlpool, position_mint, token_program)
}
const WP_METADATA_NAME: &str = "Orca Whirlpool Position";
const WP_METADATA_SYMBOL: &str = "OWP";
const WP_METADATA_URI: &str = "https://arweave.net/KZlsubXZyzeSYi2wJhyL7SY-DAot_OXhfWSYQGLmmOc";
pub fn mint_position_token_with_metadata_and_remove_authority<'info>(
whirlpool: &Account<'info, Whirlpool>,
position_mint: &Account<'info, Mint>,
position_token_account: &Account<'info, TokenAccount>,
position_metadata_account: &UncheckedAccount<'info>,
metadata_update_auth: &UncheckedAccount<'info>,
funder: &Signer<'info>,
metadata_program: &UncheckedAccount<'info>,
token_program: &Program<'info, Token>,
system_program: &Program<'info, System>,
rent: &Sysvar<'info, Rent>,
) -> ProgramResult {
mint_position_token(
whirlpool,
position_mint,
position_token_account,
token_program,
)?;
let metadata_mint_auth_account = whirlpool;
invoke_signed(
&create_metadata_accounts_v2(
metadata_program.key(),
position_metadata_account.key(),
position_mint.key(),
metadata_mint_auth_account.key(),
funder.key(),
metadata_update_auth.key(),
WP_METADATA_NAME.to_string(),
WP_METADATA_SYMBOL.to_string(),
WP_METADATA_URI.to_string(),
None,
0,
false,
true,
None,
None,
),
&[
position_metadata_account.to_account_info(),
position_mint.to_account_info(),
metadata_mint_auth_account.to_account_info(),
metadata_update_auth.to_account_info(),
funder.to_account_info(),
metadata_program.to_account_info(),
system_program.to_account_info(),
rent.to_account_info(),
],
&[&metadata_mint_auth_account.seeds()],
)?;
remove_position_token_mint_authority(whirlpool, position_mint, token_program)
}
fn mint_position_token<'info>(
whirlpool: &Account<'info, Whirlpool>,
position_mint: &Account<'info, Mint>,
position_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
) -> ProgramResult {
invoke_signed(
&mint_to(
token_program.key,
position_mint.to_account_info().key,
position_token_account.to_account_info().key,
whirlpool.to_account_info().key,
&[whirlpool.to_account_info().key],
1,
)?,
&[
position_mint.to_account_info().clone(),
position_token_account.to_account_info().clone(),
whirlpool.to_account_info().clone(),
token_program.to_account_info().clone(),
],
&[&whirlpool.seeds()],
)
}
fn remove_position_token_mint_authority<'info>(
whirlpool: &Account<'info, Whirlpool>,
position_mint: &Account<'info, Mint>,
token_program: &Program<'info, Token>,
) -> ProgramResult {
invoke_signed(
&set_authority(
token_program.key,
position_mint.to_account_info().key,
Option::None,
AuthorityType::MintTokens,
whirlpool.to_account_info().key,
&[whirlpool.to_account_info().key],
)?,
&[
position_mint.to_account_info().clone(),
whirlpool.to_account_info().clone(),
token_program.to_account_info().clone(),
],
&[&whirlpool.seeds()],
)
}

View File

@ -0,0 +1,44 @@
use anchor_lang::{
prelude::{AccountInfo, ProgramError, Pubkey, Signer},
ToAccountInfo,
};
use anchor_spl::token::TokenAccount;
use solana_program::program_option::COption;
use std::convert::TryFrom;
use crate::errors::ErrorCode;
pub fn verify_position_authority<'info>(
position_token_account: &TokenAccount,
position_authority: &Signer<'info>,
) -> Result<(), ProgramError> {
// Check token authority using validate_owner method...
match position_token_account.delegate {
COption::Some(ref delegate) if position_authority.key == delegate => {
validate_owner(delegate, &position_authority.to_account_info())?;
if position_token_account.delegated_amount != 1 {
return Err(ErrorCode::InvalidPositionTokenAmount.into());
}
}
_ => validate_owner(
&position_token_account.owner,
&position_authority.to_account_info(),
)?,
};
Ok(())
}
fn validate_owner(
expected_owner: &Pubkey,
owner_account_info: &AccountInfo,
) -> Result<(), ProgramError> {
if expected_owner != owner_account_info.key || !owner_account_info.is_signer {
return Err(ErrorCode::MissingOrInvalidDelegate.into());
}
Ok(())
}
pub fn to_timestamp_u64(t: i64) -> Result<u64, ErrorCode> {
u64::try_from(t).or(Err(ErrorCode::InvalidTimestampConversion))
}

53
scripts/calcRentExempt.ts Normal file
View File

@ -0,0 +1,53 @@
import * as anchor from "@project-serum/anchor";
import { readFile } from "mz/fs";
import Decimal from "decimal.js";
import { Keypair } from "@solana/web3.js";
const toBuffer = (arr: Buffer | Uint8Array | Array<number>): Buffer => {
if (arr instanceof Buffer) {
return arr;
} else if (arr instanceof Uint8Array) {
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
} else {
return Buffer.from(arr);
}
};
async function getKeyPair(keyPath: string): Promise<Keypair> {
const buffer = await readFile(keyPath);
let data = JSON.parse(buffer.toString());
return Keypair.fromSecretKey(toBuffer(data));
}
async function run() {
// https://api.mainnet-beta.solana.com
const wallet = new anchor.Wallet(
await getKeyPair("/Users/ottocheung/dev/solana/pub.json")
);
const connection = new anchor.web3.Connection(
"https://api.mainnet-beta.solana.com"
);
const provider = new anchor.Provider(
connection,
wallet,
anchor.Provider.defaultOptions()
);
const sizeInBytes = [
1024, 5000, 10000, 50000, 1000000, 3000000, 5000000, 10000000,
];
const solPrice = 160;
sizeInBytes.forEach(async (size) => {
const result = await provider.connection.getMinimumBalanceForRentExemption(
size
);
const sol = new Decimal(result).mul(0.000000001);
console.log(
`size - ${size} lamports - ${result} SOL- ${sol} price - ${sol.mul(
solPrice
)}`
);
});
}
run();

View File

@ -0,0 +1,15 @@
import Decimal from "decimal.js";
Decimal.set({ precision: 40, rounding: 4 });
const x64 = new Decimal(2).pow(64);
const number = new Decimal(1).mul(x64);
console.log(`number - ${number}`);
const exp = new Decimal(1.0001).sqrt().pow(1).mul(x64);
console.log(`exp - ${exp.toFixed(0, 1)}`);
const log = new Decimal(18445821805675392311)
.div(x64)
.log(new Decimal(1.0001).sqrt());
console.log(`log - ${log.toString()}`);

View File

@ -0,0 +1,68 @@
import Decimal from "decimal.js";
Decimal.set({ precision: 40, rounding: 4 });
/**
* This script is to generate the magic numbers & unit tests needed for the exponent function
* in Whirlpools.
*
* Explanation on what magic numbers are:
* https://www.notion.so/orcaso/Fixed-Point-Arithmetic-in-Whirlpools-63f9817a852b4029a6eb15dc755c7baf#5df2cfe5d62b4b0aa7e58f5f381c2d55
*/
const x32 = new Decimal(2).pow(32);
const x64 = new Decimal(2).pow(64);
const x128 = new Decimal(2).pow(128);
const b = new Decimal("1.0001");
// Qm.n = Q32.64
const n = 64;
console.log(
`Printing bit constants for whirlpool exponent of base ${b.toDecimalPlaces(
4
)}`
);
console.log(``);
console.log(`1.0001 x64 const - ${b.mul(x64).toFixed(0, 1)}`);
console.log(``);
console.log(`With a maximum tick of +/-443636, we'll need 19 bit constants:`);
for (let j = 0; j <= 18; j++) {
const power = new Decimal(2).pow(j - 1);
const sqrtBPower = b.pow(power);
const iSqrtBPower = new Decimal(1).div(sqrtBPower).mul(x64);
console.log(`${iSqrtBPower.toFixed(0, 1)}`);
}
const genUnitTestCases = (cases: number[]) => {
console.log(`tick | positive index result | negative index result`);
for (const tick of cases) {
const jsResult = new Decimal(b)
.pow(tick)
.sqrt()
.mul(new Decimal(2).pow(n))
.toFixed(0, 1);
const njsResult = new Decimal(b)
.pow(-tick)
.sqrt()
.mul(new Decimal(2).pow(n))
.toFixed(0, 1);
console.log(tick + " - " + jsResult + " , " + njsResult);
}
};
let bitGroup = [
0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384,
32768, 65536, 131072, 262144, 524288,
];
let randGroup = [2493, 23750, 395, 129, 39502, 395730, 245847, 120821].sort(
(n1, n2) => n1 - n2
);
console.log(" ");
console.log("Printing unit test cases for binary fraction bit cases:");
genUnitTestCases(bitGroup);
console.log(" ");
console.log("Printing unit test cases for random values:");
genUnitTestCases(randGroup);

View File

@ -0,0 +1,17 @@
import Decimal from "decimal.js";
Decimal.set({ toExpPos: 100, toExpNeg: -100, precision: 100 })
const b = new Decimal(1.0001);
const targetBitShift = 64;
const resolution = Decimal.pow(2, targetBitShift);
const results = [];
for (let j = 0; j < 19; j++) {
// Calculate target price
const index = Decimal.pow(2, j);
console.log("index", index);
const rawPrice = b.pow(index.div(2));
const targetPrice = rawPrice.mul(Decimal.pow(2, 96)).floor();
console.log("targetPrice", targetPrice);
}

16
scripts/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "scripts",
"version": "1.0.0",
"dependencies": {
"@project-serum/anchor": "^0.20.1",
"@solana/spl-token": "^0.1.8",
"@orca-so/whirlpool-client-sdk": "0.0.7"
},
"devDependencies": {
"@types/mocha": "^9.0.0",
"@types/mz": "^2.7.3",
"chai": "^4.3.4",
"mocha": "^9.0.3",
"mz": "^2.7.0"
}
}

20
scripts/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"composite": true,
"target": "es6",
"module": "commonjs",
"allowJs": false,
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"references": [
{
"path": "../sdk/src"
}
]
}

11
scripts/whirlpoolNft.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "Orca Whirlpool Position",
"symbol": "OWP",
"description": "Orca Whirlpool Position",
"image": "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 581.4 581.4' width='120px' height='120px'%3e%3cdefs%3e%3cstyle%3e.cls-1%7bfill:%23ffd15c;%7d%3c/style%3e%3c/defs%3e%3cg id='Layer_3' data-name='Layer 3'%3e%3cpath class='cls-1' d='M523.54,249.16a233.55,233.55,0,0,0-6.7-41.53,222.48,222.48,0,0,0-35.59-75.46,219.47,219.47,0,0,0-27.72-31.06A222.1,222.1,0,0,0,420.62,76,236.75,236.75,0,0,0,384,57.44a260.8,260.8,0,0,0-38.87-12.08,304.15,304.15,0,0,0-80-6.79,4,4,0,0,0,0,8c27.6,1.25,53.31,5.45,76.45,12.49a246,246,0,0,1,35.68,13.78,225.16,225.16,0,0,1,32.68,19.26,209.66,209.66,0,0,1,28.49,24.51,197.65,197.65,0,0,1,40.51,62A199.88,199.88,0,0,1,489.64,214a205.08,205.08,0,0,1,4.22,36.61,218.73,218.73,0,0,1-2.25,36.81A194,194,0,0,1,468.46,356a178.27,178.27,0,0,1-20.76,29.22,167.18,167.18,0,0,1-26.15,24.12,177.22,177.22,0,0,1-64.3,29.73,208.44,208.44,0,0,1-35.61,5.42c-6.45.43-12.39.61-18.26.53-1,0-1.89,0-2.79-.05l-1.47,0-1.34-.07c-.95,0-1.9-.09-2.92-.17-2.93-.2-5.91-.57-8.39-.9a125,125,0,0,1-61.23-26.55,122.38,122.38,0,0,1-38.44-53.58,106.39,106.39,0,0,1-6.14-31.94c-.13-3-.06-5.92,0-8.19s.26-4.91.6-8.2a106.41,106.41,0,0,1,3-16,96.59,96.59,0,0,1,13-28.91A92.54,92.54,0,0,1,219,247.72a102.29,102.29,0,0,1,28.35-14.82,120.85,120.85,0,0,1,32.37-6.14c2.39-.15,5.12-.22,8.35-.22a43.55,43.55,0,0,1,6.33.5,27.2,27.2,0,0,1,9.92,3.39,18.74,18.74,0,0,1,6.23,6,19.74,19.74,0,0,1,2.91,8.37,20.74,20.74,0,0,1-.92,9,19.24,19.24,0,0,1-1.81,3.88,17.87,17.87,0,0,1-2.62,3.24,18.58,18.58,0,0,1-7.5,4.38,27.88,27.88,0,0,1-5.14,1.14l-.72.08c-.24,0-.48.06-.71.07l-1.57.11-4.53.1a91.91,91.91,0,0,0-13.33,1.32A79.66,79.66,0,0,0,261,271.79a68.1,68.1,0,0,0-25.36,16.26,64.19,64.19,0,0,0-15.72,26.82,66.54,66.54,0,0,0,10.52,58.55,68.6,68.6,0,0,0,22.74,19.51,78.85,78.85,0,0,0,26.66,8.55,94.71,94.71,0,0,0,13,.94h1.6l1.39,0c1,0,1.93,0,2.84-.07,1.87-.05,3.79-.14,5.69-.26a167.14,167.14,0,0,0,45-9A148.35,148.35,0,0,0,390.39,371,140,140,0,0,0,441.5,293.6,155.89,155.89,0,0,0,446,247.79a162,162,0,0,0-8.29-44.93,152.92,152.92,0,0,0-21-40.87,155.5,155.5,0,0,0-32-32.74A167,167,0,0,0,301.71,96.4a185.25,185.25,0,0,0-22.24-1.11c-7.17.09-14.4.49-21.51,1.2a239.16,239.16,0,0,0-42.29,8.1,225.88,225.88,0,0,0-76.51,38.84,211.35,211.35,0,0,0-55.49,65.3,219.75,219.75,0,0,0-25.05,81.15A247.29,247.29,0,0,0,57.84,332a234.14,234.14,0,0,0,6.66,41.54A221.33,221.33,0,0,0,100.17,449,217.9,217.9,0,0,0,128,480,220.46,220.46,0,0,0,160.85,505a244.85,244.85,0,0,0,75.4,30.85,295.93,295.93,0,0,0,66,7.27q7,0,14.07-.31a4,4,0,0,0,0-8c-27.43-1.23-53.14-5.48-76.39-12.62-6.29-1.94-12.4-4.05-18.14-6.27-5.94-2.32-11.84-4.9-17.54-7.66A223.72,223.72,0,0,1,171.58,489a200.79,200.79,0,0,1-51.82-53.66,194.09,194.09,0,0,1-28.09-68.13,206.75,206.75,0,0,1-4.18-36.61,218.06,218.06,0,0,1,2.28-36.77,185.5,185.5,0,0,1,23.84-68.1,178.87,178.87,0,0,1,48-52.74,189,189,0,0,1,64.53-30.34,198.5,198.5,0,0,1,35.38-5.81c5.93-.44,12-.62,17.91-.53a141.29,141.29,0,0,1,17.22,1.33c23.12,3.11,44.43,12,61.64,25.75a111.63,111.63,0,0,1,37,53.48,116.89,116.89,0,0,1,5.49,32.61A110.59,110.59,0,0,1,397.11,282,96.37,96.37,0,0,1,384,310.82a93.81,93.81,0,0,1-21.79,22.79,103.63,103.63,0,0,1-28.39,14.88,120,120,0,0,1-32.31,6.19c-1.4.08-2.8.13-4.23.16-.61,0-1.21,0-1.82,0h-2.27a48.07,48.07,0,0,1-6.6-.5A31.48,31.48,0,0,1,275.78,351a20.8,20.8,0,0,1-7-6,19,19,0,0,1-3.41-7.94,18.61,18.61,0,0,1,.46-8.53,16.47,16.47,0,0,1,4.08-6.92,20.27,20.27,0,0,1,7.66-4.76,31.63,31.63,0,0,1,5.38-1.43,45.37,45.37,0,0,1,6.31-.6l5.28-.11h.18l3.31-.23c.85-.06,1.7-.16,2.55-.26l.95-.11a76.87,76.87,0,0,0,14-3.15A65.75,65.75,0,0,0,351.41,283,67.38,67.38,0,0,0,358,269.07,68.91,68.91,0,0,0,361,239.13a67.56,67.56,0,0,0-10.24-28.54,66,66,0,0,0-22-21.15A74.29,74.29,0,0,0,301.6,180a92,92,0,0,0-13.36-1c-4.5,0-8,.13-11.37.36a167.85,167.85,0,0,0-45,8.94,147.55,147.55,0,0,0-41.19,22,139.57,139.57,0,0,0-51,77.56,152.89,152.89,0,0,0-3.93,22.9c-.39,4.39-.61,8-.67,11.48-.09,4.46,0,8.13.16,11.54a151.39,151.39,0,0,0,9.4,45.25,159.53,159.53,0,0,0,21.92,40.09,169,169,0,0,0,70.93,54.75,166.11,166.11,0,0,0,43.7,11.25c4.51.48,8,.75,11.18.88,1.44.08,2.87.11,4.25.14l1.37,0h1.71c1.32,0,2.67,0,3.87,0,6.76-.09,13.92-.49,21.29-1.18a248.58,248.58,0,0,0,42.22-7.6,220.77,220.77,0,0,0,40.43-15.29,208.26,208.26,0,0,0,36.61-23.25,205.59,205.59,0,0,0,54.56-66.34,225.76,225.76,0,0,0,24.06-81.22A248.76,248.76,0,0,0,523.54,249.16Z'/%3e%3c/g%3e%3c/svg%3e",
"external_url": "https://www.orca.so/whirlpools/",
"collection": {
"name": "Orca Whirlpool Position",
"family": "Orca Whirlpool Position"
}
}

1
sdk/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.npmrc

2
sdk/.prettierignore Normal file
View File

@ -0,0 +1,2 @@
# Ignore artifacts:
src/artifacts

3
sdk/.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 100
}

3212
sdk/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
sdk/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "@orca-so/whirlpool-client-sdk",
"version": "0.0.8",
"description": "Provides functions to generate instructions needed for Orca's Whirlpool contracts",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@metaplex-foundation/mpl-token-metadata": "1.2.5",
"@project-serum/anchor": "^0.20.1",
"@solana/spl-token": "^0.1.8",
"decimal.js": "^10.3.1"
},
"devDependencies": {
"@types/decimal.js": "^7.4.0",
"@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"chai": "^4.3.4",
"eslint-config-prettier": "^8.3.0",
"mocha": "^9.0.3",
"prettier": "^2.3.2",
"typescript": "^4.5.5"
},
"scripts": {
"build": "tsc -p src",
"watch": "tsc -w -p src",
"prepublishOnly": "yarn build",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write"
},
"lint-staged": {
"*.{ts,md}": "yarn run prettier-format"
},
"files": [
"/dist"
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

379
sdk/src/client.ts Normal file
View File

@ -0,0 +1,379 @@
import { WhirlpoolContext } from "./context";
import { PublicKey } from "@solana/web3.js";
import { WhirlpoolConfigAccount } from "./types/public/account-types";
import { TransactionBuilder } from "./utils/transactions/transactions-builder";
import { buildInitializeConfigIx } from "./instructions/initialize-config-ix";
import {
ClosePositionParams,
CollectFeesParams,
CollectProtocolFeesParams,
CollectRewardParams,
InitConfigParams,
InitializeRewardParams,
InitPoolParams,
InitTickArrayParams,
OpenPositionParams,
SetCollectProtocolFeesAuthorityParams,
SetFeeAuthorityParams,
SetRewardAuthorityBySuperAuthorityParams,
parsePosition,
parseTickArray,
parseWhirlpool,
parseWhirlpoolsConfig,
SetRewardAuthorityParams,
SetRewardEmissionsParams,
SetRewardEmissionsSuperAuthorityParams,
SwapParams,
UpdateFeesAndRewardsParams,
SetFeeRateParams,
SetDefaultProtocolFeeRateParams,
SetProtocolFeeRateParams,
SetDefaultFeeRateParams,
DecreaseLiquidityParams,
IncreaseLiquidityParams,
InitFeeTierParams,
} from ".";
import { buildInitPoolIx } from "./instructions/initialize-pool-ix";
import {
FeeTierData,
PositionData,
TickArrayData,
WhirlpoolData,
} from "./types/public/anchor-types";
import {
buildOpenPositionIx,
buildOpenPositionWithMetadataIx,
} from "./instructions/open-position-ix";
import { buildInitTickArrayIx } from "./instructions/initialize-tick-array-ix";
import { buildIncreaseLiquidityIx } from "./instructions/increase-liquidity-ix";
import { buildCollectFeesIx } from "./instructions/collect-fees-ix";
import { buildCollectRewardIx } from "./instructions/collect-reward-ix";
import { buildSwapIx } from "./instructions/swap-ix";
import { buildInitializeRewardIx } from "./instructions/initialize-reward-ix";
import { buildSetRewardEmissionsSuperAuthorityIx } from "./instructions/set-reward-emissions-super-authority-ix";
import { buildSetRewardAuthorityIx } from "./instructions/set-reward-authority-ix";
import { buildSetRewardEmissionsIx } from "./instructions/set-reward-emissions-ix";
import { buildClosePositionIx } from "./instructions/close-position-ix";
import { buildSetRewardAuthorityBySuperAuthorityIx } from "./instructions/set-reward-authority-by-super-authority-ix";
import { buildSetFeeAuthorityIx } from "./instructions/set-fee-authority-ix";
import { buildSetCollectProtocolFeesAuthorityIx } from "./instructions/set-collect-protocol-fees-authority-ix";
import { buildUpdateFeesAndRewardsIx } from "./instructions/update-fees-and-rewards-ix";
import { buildCollectProtocolFeesIx } from "./instructions/collect-protocol-fees-ix";
import { buildDecreaseLiquidityIx } from "./instructions/decrease-liquidity-ix";
import { buildSetFeeRateIx } from "./instructions/set-fee-rate-ix";
import { buildSetDefaultProtocolFeeRateIx } from "./instructions/set-default-protocol-fee-rate-ix";
import { buildSetDefaultFeeRateIx } from "./instructions/set-default-fee-rate-ix";
import { buildSetProtocolFeeRateIx } from "./instructions/set-protocol-fee-rate-ix";
import { buildInitializeFeeTier } from "./instructions/initialize-fee-tier";
import { Decimal } from "decimal.js";
// Global rules for Decimals
// - 40 digits of precision for the largest number
// - 20 digits of precision for the smallest number
// - Always round towards 0 to mirror smart contract rules
Decimal.set({ precision: 40, toExpPos: 40, toExpNeg: -20, rounding: 1 });
/**
* WhirlpoolClient provides a portal to perform admin-type tasks on the Whirlpool protocol.
*/
export class WhirlpoolClient {
readonly context: WhirlpoolContext;
public constructor(context: WhirlpoolContext) {
this.context = context;
}
/**
* Construct a TransactionBuilder to initialize a WhirlpoolConfig account with the provided parameters.
* @param params Parameters to configure the initialized WhirlpoolConfig account
* @returns A TransactionBuilder to initialize a WhirlpoolConfig account with the provided parameters.
*/
public initConfigTx(params: InitConfigParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitializeConfigIx(this.context, params)
);
}
/**
* Fetches and parses a WhirlpoolConfig account.
* @param poolPubKey A public key of a WhirlpoolConfig account
* @returns A WhirlpoolConfig type containing the parameters stored on the account
*/
public async getConfig(configPubKey: PublicKey): Promise<WhirlpoolConfigAccount> {
const program = this.context.program;
const account = await program.account.whirlpoolsConfig.fetch(configPubKey);
// TODO: If we feel nice we can build a builder or something instead of casting
return account as WhirlpoolConfigAccount;
}
/**
* Parses a WhirlpoolConfig account.
* @param data A buffer containing data fetched from an account
* @returns A WhirlpoolConfig type containing the parameters stored on the account
*/
public parseConfig(data: Buffer): WhirlpoolConfigAccount | null {
return parseWhirlpoolsConfig(data);
}
/**
* Construct a TransactionBuilder to initialize a FeeTier account with the provided parameters.
* @param params Parameters to configure the initialized FeeTier account
* @returns A TransactionBuilder to initialize a FeeTier account with the provided parameters.
*/
public initFeeTierTx(params: InitFeeTierParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitializeFeeTier(this.context, params)
);
}
/**
* Fetches and parses a FeeTier account.
* @param feeTierKey A public key of a FeeTier account
* @returns A FeeTier type containing the parameters stored on the account
*/
public async getFeeTier(feeTierKey: PublicKey): Promise<FeeTierData> {
const program = this.context.program;
const feeTierAccount = await program.account.feeTier.fetch(feeTierKey);
return feeTierAccount as unknown as FeeTierData;
}
/**
* Construct a TransactionBuilder to initialize a Whirlpool account with the provided parameters.
* @param params Parameters to configure the initialized Whirlpool account
* @returns A TransactionBuilder to initialize a Whirlpool account with the provided parameters.
*/
public initPoolTx(params: InitPoolParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitPoolIx(this.context, params)
);
}
/**
* Fetches and parses a Whirlpool account.
* @param poolPubKey A public key of a Whirlpool account
* @returns A Whirlpool type containing the parameters stored on the account
*/
public async getPool(poolKey: PublicKey): Promise<WhirlpoolData> {
const program = this.context.program;
const whirlpoolAccount = await program.account.whirlpool.fetch(poolKey);
return whirlpoolAccount as unknown as WhirlpoolData;
}
/**
* Parses a Whirlpool account.
* @param data A buffer containing data fetched from an account
* @returns A Whirlpool type containing the parameters stored on the account
*/
public parsePool(data: Buffer): WhirlpoolData | null {
return parseWhirlpool(data);
}
/**
* Construct a TransactionBuilder to open a Position account.
* @param params Parameters to configure the initialized Position account.
* @returns A TransactionBuilder to initialize a Position account with the provided parameters.
*/
public openPositionTx(params: OpenPositionParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildOpenPositionIx(this.context, params)
);
}
/**
* Construct a TransactionBuilder to open a Position account with metadata.
* @param params Parameters to configure the initialized Position account.
* @returns A TransactionBuilder to initialize a Position account with the provided parameters.
*/
public openPositionWithMetadataTx(params: Required<OpenPositionParams>): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildOpenPositionWithMetadataIx(this.context, params)
);
}
public closePositionTx(params: ClosePositionParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildClosePositionIx(this.context, params)
);
}
/**
* Fetches a Position account.
* @param positionKey The public key of the Position account
* @returns A Position type containing the parameters stored on the account
*/
public async getPosition(positionKey: PublicKey): Promise<PositionData> {
const positionAccount = await this.context.program.account.position.fetch(positionKey);
return positionAccount as unknown as PositionData;
}
/**
* Parses a Position account.
* @param data A buffer containing data fetched from an account
* @returns A Position type containing the parameters stored on the account
*/
public parsePosition(data: Buffer): PositionData | null {
return parsePosition(data);
}
/*
* Construct a TransactionBuilder to initialize a TickArray account with the provided parameters.
* @param params Parameters to configure the initialized TickArray account
* @returns A TransactionBuilder to initialize a TickArray account with the provided parameters.
*/
public initTickArrayTx(params: InitTickArrayParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitTickArrayIx(this.context, params)
);
}
/**
* Fetches and parses a TickArray account. Account is used to store Ticks for a Whirlpool.
* @param arrayPubKey A public key of a TickArray account
* @returns A TickArrayData type containing the parameters stored on the account
*/
public async getTickArray(arrayPubKey: PublicKey): Promise<TickArrayData> {
const program = this.context.program;
const tickArrayAccount = await program.account.tickArray.fetch(arrayPubKey);
return tickArrayAccount as unknown as TickArrayData;
}
/**
* Parses a TickArray account.
* @param data A buffer containing data fetched from an account
* @returns A Position type containing the parameters stored on the account
*/
public parseTickArray(data: Buffer): TickArrayData | null {
return parseTickArray(data);
}
public initializeRewardTx(params: InitializeRewardParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitializeRewardIx(this.context, params)
);
}
public setRewardEmissionsTx(params: SetRewardEmissionsParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetRewardEmissionsIx(this.context, params)
);
}
/**
* Construct a TransactionBuilder to increase the liquidity of a Position.
* @param params Parameters to configure the increase liquidity instruction
* @returns A TransactionBuilder containing one increase liquidity instruction
*/
public increaseLiquidityTx(params: IncreaseLiquidityParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildIncreaseLiquidityIx(this.context, params)
);
}
public decreaseLiquidityTx(params: DecreaseLiquidityParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildDecreaseLiquidityIx(this.context, params)
);
}
public updateFeesAndRewards(params: UpdateFeesAndRewardsParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildUpdateFeesAndRewardsIx(this.context, params)
);
}
/**
* Construct a TransactionBuilder to collect the fees for a Position.
* @param params Parameters to configure the collect fees instruction
* @returns A TransactionBuilder containing one collect fees instruction
*/
public collectFeesTx(params: CollectFeesParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildCollectFeesIx(this.context, params)
);
}
/**
* Construct a TransactionBuilder to collect a reward at the specified index for a Position.
* @param params Parameters to configure the collect reward instruction
* @returns A TransactionBuilder containing one collect reward instruction
*/
public collectRewardTx(params: CollectRewardParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildCollectRewardIx(this.context, params)
);
}
public collectProtocolFeesTx(params: CollectProtocolFeesParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildCollectProtocolFeesIx(this.context, params)
);
}
public swapTx(params: SwapParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSwapIx(this.context, params)
);
}
public setRewardEmissionsSuperAuthorityTx(
params: SetRewardEmissionsSuperAuthorityParams
): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetRewardEmissionsSuperAuthorityIx(this.context, params)
);
}
public setRewardAuthorityTx(params: SetRewardAuthorityParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetRewardAuthorityIx(this.context, params)
);
}
public setRewardAuthorityBySuperAuthorityTx(
params: SetRewardAuthorityBySuperAuthorityParams
): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetRewardAuthorityBySuperAuthorityIx(this.context, params)
);
}
public setFeeAuthorityTx(params: SetFeeAuthorityParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetFeeAuthorityIx(this.context, params)
);
}
public setCollectProtocolFeesAuthorityTx(
params: SetCollectProtocolFeesAuthorityParams
): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetCollectProtocolFeesAuthorityIx(this.context, params)
);
}
public setFeeRateIx(params: SetFeeRateParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetFeeRateIx(this.context, params)
);
}
public setProtocolFeeRateIx(params: SetProtocolFeeRateParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetProtocolFeeRateIx(this.context, params)
);
}
public setDefaultFeeRateIx(params: SetDefaultFeeRateParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetDefaultFeeRateIx(this.context, params)
);
}
public setDefaultProtocolFeeRateIx(params: SetDefaultProtocolFeeRateParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetDefaultProtocolFeeRateIx(this.context, params)
);
}
}

52
sdk/src/context.ts Normal file
View File

@ -0,0 +1,52 @@
import { PublicKey, Connection, ConfirmOptions } from "@solana/web3.js";
import { Provider, Program, Idl } from "@project-serum/anchor";
import WhirlpoolIDL from "./artifacts/whirlpool.json";
import { Whirlpool } from "./artifacts/whirlpool";
import { Wallet } from "@project-serum/anchor/dist/cjs/provider";
export class WhirlpoolContext {
readonly connection: Connection;
readonly wallet: Wallet;
readonly opts: ConfirmOptions;
readonly program: Program<Whirlpool>;
readonly provider: Provider;
public static from(
connection: Connection,
wallet: Wallet,
programId: PublicKey,
opts: ConfirmOptions = Provider.defaultOptions()
): WhirlpoolContext {
const provider = new Provider(connection, wallet, opts);
const program = new Program(WhirlpoolIDL as Idl, programId, provider);
return new WhirlpoolContext(provider, program, opts);
}
public static fromWorkspace(
provider: Provider,
program: Program,
opts: ConfirmOptions = Provider.defaultOptions()
) {
return new WhirlpoolContext(provider, program, opts);
}
public static withProvider(
provider: Provider,
programId: PublicKey,
opts: ConfirmOptions = Provider.defaultOptions()
): WhirlpoolContext {
const program = new Program(WhirlpoolIDL as Idl, programId, provider);
return new WhirlpoolContext(provider, program, opts);
}
public constructor(provider: Provider, program: Program, opts: ConfirmOptions) {
this.connection = provider.connection;
this.wallet = provider.wallet;
this.opts = opts;
// It's a hack but it works on Anchor workspace *shrug*
this.program = program as unknown as Program<Whirlpool>;
this.provider = provider;
}
// TODO: Add another factory method to build from on-chain IDL
}

7
sdk/src/index.ts Normal file
View File

@ -0,0 +1,7 @@
export * from "./client";
export * from "./context";
export * from "./types/public";
export * from "./utils/public";
export * from "./types/public/anchor-types";
export * from "./utils/transactions/transactions-builder";

View File

@ -0,0 +1,28 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { ClosePositionParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
export function buildClosePositionIx(
context: WhirlpoolContext,
params: ClosePositionParams
): Instruction {
const { positionAuthority, receiver, position, positionMint, positionTokenAccount } = params;
const ix = context.program.instruction.closePosition({
accounts: {
positionAuthority,
receiver,
position,
positionMint,
positionTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,40 @@
import { WhirlpoolContext } from "../context";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Instruction } from "../utils/transactions/transactions-builder";
import { CollectFeesParams } from "..";
export function buildCollectFeesIx(
context: WhirlpoolContext,
params: CollectFeesParams
): Instruction {
const {
whirlpool,
positionAuthority,
position,
positionTokenAccount,
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenVaultA,
tokenVaultB,
} = params;
const ix = context.program.instruction.collectFees({
accounts: {
whirlpool,
positionAuthority,
position,
positionTokenAccount,
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenVaultA,
tokenVaultB,
tokenProgram: TOKEN_PROGRAM_ID,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,38 @@
import { WhirlpoolContext } from "../context";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Instruction } from "../utils/transactions/transactions-builder";
import { CollectProtocolFeesParams } from "..";
export function buildCollectProtocolFeesIx(
context: WhirlpoolContext,
params: CollectProtocolFeesParams
): Instruction {
const {
whirlpoolsConfig,
whirlpool,
collectProtocolFeesAuthority,
tokenVaultA,
tokenVaultB,
tokenDestinationA,
tokenDestinationB,
} = params;
const ix = context.program.instruction.collectProtocolFees({
accounts: {
whirlpoolsConfig,
whirlpool,
collectProtocolFeesAuthority,
tokenVaultA,
tokenVaultB,
tokenDestinationA,
tokenDestinationB,
tokenProgram: TOKEN_PROGRAM_ID,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,37 @@
import { WhirlpoolContext } from "../context";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Instruction } from "../utils/transactions/transactions-builder";
import { CollectRewardParams } from "..";
export function buildCollectRewardIx(
context: WhirlpoolContext,
params: CollectRewardParams
): Instruction {
const {
whirlpool,
positionAuthority,
position,
positionTokenAccount,
rewardOwnerAccount,
rewardVault,
rewardIndex,
} = params;
const ix = context.program.instruction.collectReward(rewardIndex, {
accounts: {
whirlpool,
positionAuthority,
position,
positionTokenAccount,
rewardOwnerAccount,
rewardVault,
tokenProgram: TOKEN_PROGRAM_ID,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,47 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { DecreaseLiquidityParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
export function buildDecreaseLiquidityIx(
context: WhirlpoolContext,
params: DecreaseLiquidityParams
): Instruction {
const {
liquidityAmount,
tokenMinA,
tokenMinB,
whirlpool,
positionAuthority,
position,
positionTokenAccount,
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenVaultA,
tokenVaultB,
tickArrayLower,
tickArrayUpper,
} = params;
const ix = context.program.instruction.decreaseLiquidity(liquidityAmount, tokenMinA, tokenMinB, {
accounts: {
whirlpool,
tokenProgram: TOKEN_PROGRAM_ID,
positionAuthority,
position,
positionTokenAccount,
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenVaultA,
tokenVaultB,
tickArrayLower,
tickArrayUpper,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,47 @@
import { WhirlpoolContext } from "../context";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Instruction } from "../utils/transactions/transactions-builder";
import { IncreaseLiquidityParams } from "..";
export function buildIncreaseLiquidityIx(
context: WhirlpoolContext,
params: IncreaseLiquidityParams
): Instruction {
const {
liquidityAmount,
tokenMaxA,
tokenMaxB,
whirlpool,
positionAuthority,
position,
positionTokenAccount,
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenVaultA,
tokenVaultB,
tickArrayLower,
tickArrayUpper,
} = params;
const ix = context.program.instruction.increaseLiquidity(liquidityAmount, tokenMaxA, tokenMaxB, {
accounts: {
whirlpool,
tokenProgram: TOKEN_PROGRAM_ID,
positionAuthority,
position,
positionTokenAccount,
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenVaultA,
tokenVaultB,
tickArrayLower,
tickArrayUpper,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,37 @@
import { SystemProgram } from "@solana/web3.js";
import { WhirlpoolContext } from "../context";
import { InitConfigParams } from "../types/public/ix-types";
import { Instruction } from "../utils/transactions/transactions-builder";
export function buildInitializeConfigIx(
context: WhirlpoolContext,
params: InitConfigParams
): Instruction {
const {
feeAuthority,
collectProtocolFeesAuthority,
rewardEmissionsSuperAuthority,
defaultProtocolFeeRate,
funder,
} = params;
const ix = context.program.instruction.initializeConfig(
feeAuthority,
collectProtocolFeesAuthority,
rewardEmissionsSuperAuthority,
defaultProtocolFeeRate,
{
accounts: {
config: params.whirlpoolConfigKeypair.publicKey,
funder,
systemProgram: SystemProgram.programId,
},
}
);
return {
instructions: [ix],
cleanupInstructions: [],
signers: [params.whirlpoolConfigKeypair],
};
}

View File

@ -0,0 +1,29 @@
import { SystemProgram } from "@solana/web3.js";
import { getFeeTierPda } from "..";
import { WhirlpoolContext } from "../context";
import { InitFeeTierParams } from "../types/public/ix-types";
import { Instruction } from "../utils/transactions/transactions-builder";
export function buildInitializeFeeTier(
context: WhirlpoolContext,
params: InitFeeTierParams
): Instruction {
const { feeTierPda, whirlpoolConfigKey, tickSpacing, feeAuthority, defaultFeeRate, funder } =
params;
const ix = context.program.instruction.initializeFeeTier(tickSpacing, defaultFeeRate, {
accounts: {
config: whirlpoolConfigKey,
feeTier: feeTierPda.publicKey,
feeAuthority,
funder,
systemProgram: SystemProgram.programId,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,49 @@
import { WhirlpoolContext } from "../context";
import { SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js";
import { Instruction } from "../utils/transactions/transactions-builder";
import { InitPoolParams } from "..";
import { WhirlpoolBumpsData } from "../types/public/anchor-types";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
export function buildInitPoolIx(context: WhirlpoolContext, params: InitPoolParams): Instruction {
const program = context.program;
const {
initSqrtPrice,
tokenMintA,
tokenMintB,
whirlpoolConfigKey,
whirlpoolPda,
feeTierKey,
tokenVaultAKeypair,
tokenVaultBKeypair,
tickSpacing,
funder,
} = params;
const whirlpoolBumps: WhirlpoolBumpsData = {
whirlpoolBump: whirlpoolPda.bump,
};
const ix = program.instruction.initializePool(whirlpoolBumps, tickSpacing, initSqrtPrice, {
accounts: {
whirlpoolsConfig: whirlpoolConfigKey,
tokenMintA: tokenMintA,
tokenMintB: tokenMintB,
funder,
whirlpool: whirlpoolPda.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
feeTier: feeTierKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: SYSVAR_RENT_PUBKEY,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [tokenVaultAKeypair, tokenVaultBKeypair],
};
}

View File

@ -0,0 +1,33 @@
import * as anchor from "@project-serum/anchor";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { SystemProgram } from "@solana/web3.js";
import { InitializeRewardParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
export function buildInitializeRewardIx(
context: WhirlpoolContext,
params: InitializeRewardParams
): Instruction {
const { rewardAuthority, funder, whirlpool, rewardMint, rewardVaultKeypair, rewardIndex } =
params;
const ix = context.program.instruction.initializeReward(rewardIndex, {
accounts: {
rewardAuthority,
funder,
whirlpool,
rewardMint,
rewardVault: rewardVaultKeypair.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [rewardVaultKeypair],
};
}

View File

@ -0,0 +1,28 @@
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { InitTickArrayParams } from "..";
import * as anchor from "@project-serum/anchor";
export function buildInitTickArrayIx(
context: WhirlpoolContext,
params: InitTickArrayParams
): Instruction {
const program = context.program;
const { whirlpool, funder, tickArrayPda } = params;
const ix = program.instruction.initializeTickArray(params.startTick, {
accounts: {
whirlpool,
funder,
tickArray: tickArrayPda.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,87 @@
import { WhirlpoolContext } from "../context";
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { Instruction } from "../utils/transactions/transactions-builder";
import { OpenPositionParams, PDA } from "..";
import * as anchor from "@project-serum/anchor";
import {
OpenPositionBumpsData,
OpenPositionWithMetadataBumpsData,
} from "../types/public/anchor-types";
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { METADATA_PROGRAM_ADDRESS } from "../utils/public";
export function buildOpenPositionIx(
context: WhirlpoolContext,
params: OpenPositionParams
): Instruction {
const { positionPda, tickLowerIndex, tickUpperIndex } = params;
const bumps: OpenPositionBumpsData = {
positionBump: positionPda.bump,
};
const ix = context.program.instruction.openPosition(bumps, tickLowerIndex, tickUpperIndex, {
accounts: openPositionAccounts(params),
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}
export function buildOpenPositionWithMetadataIx(
context: WhirlpoolContext,
params: OpenPositionParams & { metadataPda: PDA }
): Instruction {
const { positionPda, metadataPda, tickLowerIndex, tickUpperIndex } = params;
const bumps: OpenPositionWithMetadataBumpsData = {
positionBump: positionPda.bump,
metadataBump: metadataPda.bump,
};
const ix = context.program.instruction.openPositionWithMetadata(
bumps,
tickLowerIndex,
tickUpperIndex,
{
accounts: {
...openPositionAccounts(params),
positionMetadataAccount: metadataPda.publicKey,
metadataProgram: METADATA_PROGRAM_ADDRESS,
metadataUpdateAuth: new PublicKey("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"),
},
}
);
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}
export function openPositionAccounts(params: OpenPositionParams) {
const {
funder,
ownerKey,
positionPda,
positionMintAddress,
positionTokenAccountAddress,
whirlpoolKey,
} = params;
return {
funder: funder,
owner: ownerKey,
position: positionPda.publicKey,
positionMint: positionMintAddress,
positionTokenAccount: positionTokenAccountAddress,
whirlpool: whirlpoolKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
};
}

Some files were not shown because too many files have changed in this diff Show More