Open Source commit for Orca Whirlpools (#1)
This commit is contained in:
parent
f0b4de883f
commit
c55588a6ea
|
@ -0,0 +1,9 @@
|
|||
.anchor
|
||||
.DS_Store
|
||||
.vscode/
|
||||
target
|
||||
**/*.rs.bk
|
||||
node_modules
|
||||
test-ledger/
|
||||
sdk/dist/
|
||||
sdk/node_modules
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"files.insertFinalNewline": true
|
||||
}
|
|
@ -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"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -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
|
|
@ -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.
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"]
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,3 @@
|
|||
pub mod test_constants;
|
||||
|
||||
pub use test_constants::*;
|
|
@ -0,0 +1,9 @@
|
|||
#[cfg(test)]
|
||||
use anchor_lang::prelude::Pubkey;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test_program_id() -> Pubkey {
|
||||
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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,
|
||||
)?)
|
||||
}
|
|
@ -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)?)
|
||||
}
|
|
@ -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(),
|
||||
)?)
|
||||
}
|
|
@ -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(),
|
||||
)?)
|
||||
}
|
|
@ -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)?)
|
||||
}
|
|
@ -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::*;
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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(),
|
||||
))
|
||||
}
|
|
@ -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)?)
|
||||
}
|
|
@ -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)?)
|
||||
}
|
|
@ -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()))
|
||||
}
|
|
@ -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)?)
|
||||
}
|
|
@ -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)?)
|
||||
}
|
|
@ -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(),
|
||||
)?)
|
||||
}
|
|
@ -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(),
|
||||
)?)
|
||||
}
|
|
@ -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,
|
||||
)?)
|
||||
}
|
|
@ -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(),
|
||||
))
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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
|
@ -0,0 +1,5 @@
|
|||
pub mod liquidity_manager;
|
||||
pub mod position_manager;
|
||||
pub mod swap_manager;
|
||||
pub mod tick_manager;
|
||||
pub mod whirlpool_manager;
|
|
@ -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
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
#[cfg(test)]
|
||||
mod swap_integration_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use swap_integration_tests::*;
|
|
@ -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
|
@ -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::*;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
pub mod liquidity_test_fixture;
|
||||
pub mod swap_test_fixture;
|
||||
|
||||
pub use liquidity_test_fixture::*;
|
||||
pub use swap_test_fixture::*;
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()],
|
||||
)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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();
|
|
@ -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()}`);
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
.npmrc
|
|
@ -0,0 +1,2 @@
|
|||
# Ignore artifacts:
|
||||
src/artifacts
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"printWidth": 100
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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";
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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],
|
||||
};
|
||||
}
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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],
|
||||
};
|
||||
}
|
|
@ -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],
|
||||
};
|
||||
}
|
|
@ -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: [],
|
||||
};
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue