mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-05-18 15:09:51 +00:00
feat(amm)!: introduce minimum liquidity lock on pool initialization
Permanently lock `MINIMUM_LIQUIDITY` (1_000) LP tokens in a dedicated LP-lock holding PDA on pool creation, following the Uniswap v2 "dead shares" pattern. The pool creator receives `initial_lp - MINIMUM_LIQUIDITY` tokens instead of the full initial_lp amount. Adds `compute_lp_lock_holding_pda` and `LP_LOCK_HOLDING_PDA_SEED` to amm_core, updates new_definition to emit two sequential chained calls (create LP definition + lock holding, then mint user share), and adjusts remove liquidity to account for the permanently locked floor. BREAKING CHANGE: NewDefinition instruction requires an additional LP-lock holding account derived via `compute_lp_lock_holding_pda(amm_program_id, pool_id)`.
This commit is contained in:
parent
f89a8f9865
commit
fddd6e15bd
@ -29,6 +29,12 @@
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "lp_lock_holding",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding_a",
|
||||
"writable": false,
|
||||
|
||||
@ -7,16 +7,26 @@ use nssa_core::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// These stable seed bytes are part of the PDA derivation scheme and must stay unchanged for
|
||||
// compatibility.
|
||||
const LIQUIDITY_TOKEN_PDA_SEED: [u8; 32] = [0; 32];
|
||||
const LP_LOCK_HOLDING_PDA_SEED: [u8; 32] = [1; 32];
|
||||
|
||||
/// AMM Program Instruction.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Initializes a new Pool (or re-initializes an inactive Pool).
|
||||
///
|
||||
/// On initialization, `MINIMUM_LIQUIDITY` LP tokens are permanently locked
|
||||
/// in the LP-lock holding PDA; the caller receives `initial_lp - MINIMUM_LIQUIDITY`.
|
||||
///
|
||||
/// Required accounts:
|
||||
/// - AMM Pool
|
||||
/// - Vault Holding Account for Token A
|
||||
/// - Vault Holding Account for Token B
|
||||
/// - Pool Liquidity Token Definition
|
||||
/// - LP Lock Holding Account, derived as `compute_lp_lock_holding_pda(amm_program_id,
|
||||
/// pool.account_id)`
|
||||
/// - User Holding Account for Token A (authorized)
|
||||
/// - User Holding Account for Token B (authorized)
|
||||
/// - User Holding Account for Pool Liquidity
|
||||
@ -75,6 +85,8 @@ pub enum Instruction {
|
||||
},
|
||||
}
|
||||
|
||||
pub const MINIMUM_LIQUIDITY: u128 = 1_000;
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PoolDefinition {
|
||||
pub definition_token_a_id: AccountId,
|
||||
@ -87,9 +99,13 @@ pub struct PoolDefinition {
|
||||
pub reserve_b: u128,
|
||||
/// Fees are currently not used
|
||||
pub fees: u128,
|
||||
/// A pool becomes inactive (active = false)
|
||||
/// once all of its liquidity has been removed (e.g., reserves are emptied and
|
||||
/// liquidity_pool_supply = 0)
|
||||
/// Indicates whether the pool is initialized for use.
|
||||
/// `MINIMUM_LIQUIDITY` LP tokens are permanently locked at initialization
|
||||
/// and cannot be removed, so `liquidity_pool_supply` will never drop below
|
||||
/// `MINIMUM_LIQUIDITY` for pools created after the minimum-liquidity lock
|
||||
/// was introduced. Reaching that floor does not by itself imply
|
||||
/// `active = false`; pools may remain active with only the permanently
|
||||
/// locked minimum liquidity remaining.
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
@ -186,7 +202,26 @@ pub fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed {
|
||||
|
||||
let mut bytes = [0; 64];
|
||||
bytes[0..32].copy_from_slice(&pool_id.to_bytes());
|
||||
bytes[32..].copy_from_slice(&[0; 32]);
|
||||
bytes[32..].copy_from_slice(&LIQUIDITY_TOKEN_PDA_SEED);
|
||||
|
||||
PdaSeed::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.expect("Hash output must be exactly 32 bytes long"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn compute_lp_lock_holding_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId {
|
||||
AccountId::from((&amm_program_id, &compute_lp_lock_holding_pda_seed(pool_id)))
|
||||
}
|
||||
|
||||
pub fn compute_lp_lock_holding_pda_seed(pool_id: AccountId) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256};
|
||||
|
||||
let mut bytes = [0; 64];
|
||||
bytes[0..32].copy_from_slice(&pool_id.to_bytes());
|
||||
bytes[32..].copy_from_slice(&LP_LOCK_HOLDING_PDA_SEED);
|
||||
|
||||
PdaSeed::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
|
||||
@ -22,6 +22,7 @@ mod amm {
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
pool_definition_lp: AccountWithMetadata,
|
||||
lp_lock_holding: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
user_holding_lp: AccountWithMetadata,
|
||||
@ -34,6 +35,7 @@ mod amm {
|
||||
vault_a,
|
||||
vault_b,
|
||||
pool_definition_lp,
|
||||
lp_lock_holding,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
user_holding_lp,
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{
|
||||
compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_pool_pda,
|
||||
compute_vault_pda, PoolDefinition,
|
||||
compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda,
|
||||
compute_pool_pda, compute_vault_pda, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use token_core::TokenDefinition;
|
||||
|
||||
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||
pub fn new_definition(
|
||||
@ -15,6 +16,7 @@ pub fn new_definition(
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
pool_definition_lp: AccountWithMetadata,
|
||||
lp_lock_holding: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
user_holding_lp: AccountWithMetadata,
|
||||
@ -61,10 +63,16 @@ pub fn new_definition(
|
||||
compute_liquidity_token_pda(amm_program_id, pool.account_id),
|
||||
"Liquidity pool Token Definition Account ID does not match PDA"
|
||||
);
|
||||
assert_eq!(
|
||||
lp_lock_holding.account_id,
|
||||
compute_lp_lock_holding_pda(amm_program_id, pool.account_id),
|
||||
"LP lock holding Account ID does not match PDA"
|
||||
);
|
||||
|
||||
// TODO: return here
|
||||
// Verify that Pool Account is not active
|
||||
let pool_account_data = if pool.account == Account::default() {
|
||||
let is_new_pool = pool.account == Account::default();
|
||||
let pool_account_data = if is_new_pool {
|
||||
PoolDefinition::default()
|
||||
} else {
|
||||
PoolDefinition::try_from(&pool.account.data)
|
||||
@ -75,9 +83,20 @@ pub fn new_definition(
|
||||
!pool_account_data.active,
|
||||
"Cannot initialize an active Pool Definition"
|
||||
);
|
||||
if !is_new_pool {
|
||||
assert_eq!(
|
||||
pool_account_data.liquidity_pool_supply, 0,
|
||||
"New definition: inactive Pool Definition must have zero LP supply before reinitialization"
|
||||
);
|
||||
}
|
||||
|
||||
// LP Token minting calculation
|
||||
let initial_lp = (token_a_amount.get() * token_b_amount.get()).isqrt();
|
||||
assert!(
|
||||
initial_lp > MINIMUM_LIQUIDITY,
|
||||
"Initial liquidity must exceed minimum liquidity lock"
|
||||
);
|
||||
let user_lp = initial_lp - MINIMUM_LIQUIDITY;
|
||||
|
||||
// Update pool account
|
||||
let mut pool_post = pool.account.clone();
|
||||
@ -120,39 +139,87 @@ pub fn new_definition(
|
||||
},
|
||||
);
|
||||
|
||||
// Chain call for liquidity token (TokenLP definition -> User LP Holding)
|
||||
let instruction = if pool.account == Account::default() {
|
||||
// Chain call for liquidity token lock holding
|
||||
let lock_instruction = if is_new_pool {
|
||||
token_core::Instruction::NewFungibleDefinition {
|
||||
name: String::from("LP Token"),
|
||||
total_supply: initial_lp,
|
||||
total_supply: MINIMUM_LIQUIDITY,
|
||||
}
|
||||
} else {
|
||||
token_core::Instruction::Mint {
|
||||
amount_to_mint: initial_lp,
|
||||
amount_to_mint: MINIMUM_LIQUIDITY,
|
||||
}
|
||||
};
|
||||
|
||||
let mut pool_lp_auth = pool_definition_lp.clone();
|
||||
pool_lp_auth.is_authorized = true;
|
||||
|
||||
let call_token_lp = ChainedCall::new(
|
||||
let call_token_lp_lock = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![pool_lp_auth.clone(), user_holding_lp.clone()],
|
||||
&instruction,
|
||||
vec![pool_lp_auth.clone(), lp_lock_holding.clone()],
|
||||
&lock_instruction,
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
|
||||
|
||||
let chained_calls = vec![call_token_lp, call_token_b, call_token_a];
|
||||
let mut pool_lp_after_lock = pool_lp_auth.clone();
|
||||
if pool_definition_lp.account == Account::default() {
|
||||
pool_lp_after_lock.account.program_owner = token_program_id;
|
||||
pool_lp_after_lock.account.data = Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("LP Token"),
|
||||
total_supply: MINIMUM_LIQUIDITY,
|
||||
metadata_id: None,
|
||||
});
|
||||
} else {
|
||||
let token_definition = TokenDefinition::try_from(&pool_definition_lp.account.data)
|
||||
.expect("New definition: AMM Program expects a valid LP Token Definition Account");
|
||||
let TokenDefinition::Fungible {
|
||||
name,
|
||||
total_supply,
|
||||
metadata_id,
|
||||
} = token_definition
|
||||
else {
|
||||
panic!("New definition: LP Token Definition Account must be fungible");
|
||||
};
|
||||
assert_eq!(
|
||||
total_supply, 0,
|
||||
"New definition: existing LP Token Definition Account must have zero supply before reinitialization"
|
||||
);
|
||||
|
||||
pool_lp_after_lock.account.data = Data::from(&TokenDefinition::Fungible {
|
||||
name,
|
||||
total_supply: total_supply
|
||||
.checked_add(MINIMUM_LIQUIDITY)
|
||||
.expect("LP total supply overflow on lock mint"),
|
||||
metadata_id,
|
||||
});
|
||||
}
|
||||
|
||||
let call_token_lp_user = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![pool_lp_after_lock, user_holding_lp.clone()],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: user_lp,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
|
||||
|
||||
let chained_calls = vec![
|
||||
call_token_lp_lock,
|
||||
call_token_lp_user,
|
||||
call_token_b,
|
||||
call_token_a,
|
||||
];
|
||||
|
||||
let post_states = vec![
|
||||
pool_post.clone(),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
AccountPostState::new(pool_definition_lp.account.clone()),
|
||||
AccountPostState::new(lp_lock_holding.account.clone()),
|
||||
AccountPostState::new(user_holding_a.account.clone()),
|
||||
AccountPostState::new(user_holding_b.account.clone()),
|
||||
AccountPostState::new(user_holding_lp.account.clone()),
|
||||
];
|
||||
|
||||
(post_states.clone(), chained_calls)
|
||||
(post_states, chained_calls)
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{compute_liquidity_token_pda_seed, compute_vault_pda_seed, PoolDefinition};
|
||||
use amm_core::{
|
||||
compute_liquidity_token_pda_seed, compute_vault_pda_seed, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
@ -78,6 +80,19 @@ pub fn remove_liquidity(
|
||||
pool_def_data.liquidity_pool_id,
|
||||
"Invalid liquidity account provided"
|
||||
);
|
||||
// Honest flows should never reach the permanent lock through a valid remove instruction, but
|
||||
// we still reject legacy or corrupted states that are already at the locked floor.
|
||||
assert!(
|
||||
pool_def_data.liquidity_pool_supply > MINIMUM_LIQUIDITY,
|
||||
"Pool only contains locked liquidity"
|
||||
);
|
||||
let unlocked_liquidity = pool_def_data.liquidity_pool_supply - MINIMUM_LIQUIDITY;
|
||||
// The remove instruction never sees the LP lock account directly, so we must still refuse any
|
||||
// request that would burn through the permanent floor even if ownership is already corrupted.
|
||||
assert!(
|
||||
remove_liquidity_amount <= unlocked_liquidity,
|
||||
"Cannot remove locked minimum liquidity"
|
||||
);
|
||||
|
||||
let withdraw_amount_a =
|
||||
(pool_def_data.reserve_a * remove_liquidity_amount) / pool_def_data.liquidity_pool_supply;
|
||||
@ -95,10 +110,7 @@ pub fn remove_liquidity(
|
||||
);
|
||||
|
||||
// 4. Calculate LP to reduce cap by
|
||||
let delta_lp: u128 = (pool_def_data.liquidity_pool_supply * remove_liquidity_amount)
|
||||
/ pool_def_data.liquidity_pool_supply;
|
||||
|
||||
let active: bool = pool_def_data.liquidity_pool_supply - delta_lp != 0;
|
||||
let delta_lp: u128 = remove_liquidity_amount;
|
||||
|
||||
// 5. Update pool account
|
||||
let mut pool_post = pool.account.clone();
|
||||
@ -106,7 +118,7 @@ pub fn remove_liquidity(
|
||||
liquidity_pool_supply: pool_def_data.liquidity_pool_supply - delta_lp,
|
||||
reserve_a: pool_def_data.reserve_a - withdraw_amount_a,
|
||||
reserve_b: pool_def_data.reserve_b - withdraw_amount_b,
|
||||
active,
|
||||
active: true,
|
||||
..pool_def_data.clone()
|
||||
};
|
||||
|
||||
|
||||
540
amm/src/tests.rs
540
amm/src/tests.rs
@ -3,8 +3,8 @@
|
||||
use std::num::NonZero;
|
||||
|
||||
use amm_core::{
|
||||
compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_pool_pda,
|
||||
compute_vault_pda, compute_vault_pda_seed, PoolDefinition,
|
||||
compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda,
|
||||
compute_pool_pda, compute_vault_pda, compute_vault_pda_seed, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
||||
@ -23,14 +23,15 @@ struct BalanceForTests;
|
||||
struct ChainedCallForTests;
|
||||
struct IdForTests;
|
||||
struct AccountWithMetadataForTests;
|
||||
type AccountForTests = AccountWithMetadataForTests;
|
||||
|
||||
impl BalanceForTests {
|
||||
fn vault_a_reserve_init() -> u128 {
|
||||
1_000
|
||||
5_000
|
||||
}
|
||||
|
||||
fn vault_b_reserve_init() -> u128 {
|
||||
500
|
||||
2_500
|
||||
}
|
||||
|
||||
fn vault_a_reserve_low() -> u128 {
|
||||
@ -50,11 +51,11 @@ impl BalanceForTests {
|
||||
}
|
||||
|
||||
fn user_token_a_balance() -> u128 {
|
||||
1_000
|
||||
10_000
|
||||
}
|
||||
|
||||
fn user_token_b_balance() -> u128 {
|
||||
500
|
||||
10_000
|
||||
}
|
||||
|
||||
fn user_token_lp_balance() -> u128 {
|
||||
@ -106,52 +107,93 @@ impl BalanceForTests {
|
||||
}
|
||||
|
||||
fn lp_supply_init() -> u128 {
|
||||
// sqrt(vault_a_reserve_init * vault_b_reserve_init) = sqrt(1000 * 500) = 707
|
||||
// sqrt(vault_a_reserve_init * vault_b_reserve_init) = sqrt(5000 * 2500) = 3535
|
||||
(BalanceForTests::vault_a_reserve_init() * BalanceForTests::vault_b_reserve_init()).isqrt()
|
||||
}
|
||||
|
||||
fn lp_user_init() -> u128 {
|
||||
BalanceForTests::lp_supply_init() - MINIMUM_LIQUIDITY
|
||||
}
|
||||
|
||||
fn vault_a_swap_test_1() -> u128 {
|
||||
1_500
|
||||
BalanceForTests::vault_a_reserve_init() + BalanceForTests::add_max_amount_a()
|
||||
}
|
||||
|
||||
fn vault_a_swap_test_2() -> u128 {
|
||||
715
|
||||
BalanceForTests::vault_a_reserve_init() - BalanceForTests::swap_amount_out_a()
|
||||
}
|
||||
|
||||
fn vault_b_swap_test_1() -> u128 {
|
||||
334
|
||||
BalanceForTests::vault_b_reserve_init() - BalanceForTests::swap_amount_out_b()
|
||||
}
|
||||
|
||||
fn vault_b_swap_test_2() -> u128 {
|
||||
700
|
||||
BalanceForTests::vault_b_reserve_init() + BalanceForTests::add_max_amount_b()
|
||||
}
|
||||
|
||||
fn min_amount_out() -> u128 {
|
||||
200
|
||||
}
|
||||
|
||||
fn min_amount_out_too_high() -> u128 {
|
||||
BalanceForTests::swap_amount_out_b() + 1
|
||||
}
|
||||
|
||||
fn vault_a_add_successful() -> u128 {
|
||||
1_400
|
||||
BalanceForTests::vault_a_reserve_init() + BalanceForTests::add_successful_amount_a()
|
||||
}
|
||||
|
||||
fn vault_b_add_successful() -> u128 {
|
||||
700
|
||||
BalanceForTests::vault_b_reserve_init() + BalanceForTests::add_successful_amount_b()
|
||||
}
|
||||
|
||||
fn add_successful_amount_a() -> u128 {
|
||||
400
|
||||
(BalanceForTests::vault_a_reserve_init() * BalanceForTests::add_max_amount_b())
|
||||
/ BalanceForTests::vault_b_reserve_init()
|
||||
}
|
||||
|
||||
fn add_successful_amount_b() -> u128 {
|
||||
200
|
||||
BalanceForTests::add_max_amount_b()
|
||||
}
|
||||
|
||||
fn vault_a_remove_successful() -> u128 {
|
||||
859
|
||||
BalanceForTests::vault_a_reserve_init() - BalanceForTests::remove_actual_a_successful()
|
||||
}
|
||||
|
||||
fn vault_b_remove_successful() -> u128 {
|
||||
430
|
||||
BalanceForTests::vault_b_reserve_init() - BalanceForTests::remove_actual_b_successful()
|
||||
}
|
||||
|
||||
fn swap_amount_out_b() -> u128 {
|
||||
(BalanceForTests::vault_b_reserve_init() * BalanceForTests::add_max_amount_a())
|
||||
/ (BalanceForTests::vault_a_reserve_init() + BalanceForTests::add_max_amount_a())
|
||||
}
|
||||
|
||||
fn swap_amount_out_a() -> u128 {
|
||||
(BalanceForTests::vault_a_reserve_init() * BalanceForTests::add_max_amount_b())
|
||||
/ (BalanceForTests::vault_b_reserve_init() + BalanceForTests::add_max_amount_b())
|
||||
}
|
||||
|
||||
fn add_delta_lp_successful() -> u128 {
|
||||
std::cmp::min(
|
||||
BalanceForTests::lp_supply_init() * BalanceForTests::add_successful_amount_a()
|
||||
/ BalanceForTests::vault_a_reserve_init(),
|
||||
BalanceForTests::lp_supply_init() * BalanceForTests::add_successful_amount_b()
|
||||
/ BalanceForTests::vault_b_reserve_init(),
|
||||
)
|
||||
}
|
||||
|
||||
fn remove_actual_b_successful() -> u128 {
|
||||
(BalanceForTests::vault_b_reserve_init() * BalanceForTests::remove_amount_lp())
|
||||
/ BalanceForTests::lp_supply_init()
|
||||
}
|
||||
|
||||
fn add_lp_supply_successful() -> u128 {
|
||||
BalanceForTests::lp_supply_init() + BalanceForTests::add_delta_lp_successful()
|
||||
}
|
||||
|
||||
fn remove_lp_supply_successful() -> u128 {
|
||||
BalanceForTests::lp_supply_init() - BalanceForTests::remove_amount_lp()
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,7 +212,7 @@ impl ChainedCallForTests {
|
||||
}
|
||||
|
||||
fn cc_swap_token_b_test_1() -> ChainedCall {
|
||||
let swap_amount: u128 = 166;
|
||||
let swap_amount = BalanceForTests::swap_amount_out_b();
|
||||
|
||||
let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init();
|
||||
vault_b_auth.is_authorized = true;
|
||||
@ -189,7 +231,7 @@ impl ChainedCallForTests {
|
||||
}
|
||||
|
||||
fn cc_swap_token_a_test_2() -> ChainedCall {
|
||||
let swap_amount: u128 = 285;
|
||||
let swap_amount = BalanceForTests::swap_amount_out_a();
|
||||
|
||||
let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init();
|
||||
vault_a_auth.is_authorized = true;
|
||||
@ -257,7 +299,7 @@ impl ChainedCallForTests {
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: 282,
|
||||
amount_to_mint: BalanceForTests::add_delta_lp_successful(),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
|
||||
@ -290,7 +332,7 @@ impl ChainedCallForTests {
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![vault_b_auth, AccountWithMetadataForTests::user_holding_b()],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: 70,
|
||||
amount_to_transfer: BalanceForTests::remove_actual_b_successful(),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||
@ -326,7 +368,7 @@ impl ChainedCallForTests {
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: BalanceForTests::add_successful_amount_a(),
|
||||
amount_to_transfer: BalanceForTests::vault_a_reserve_init(),
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -339,20 +381,36 @@ impl ChainedCallForTests {
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: BalanceForTests::add_successful_amount_b(),
|
||||
amount_to_transfer: BalanceForTests::vault_b_reserve_init(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn cc_new_definition_token_lp() -> ChainedCall {
|
||||
fn cc_new_definition_token_lp_lock() -> ChainedCall {
|
||||
let mut pool_lp_auth = AccountForTests::pool_lp_reinitializable();
|
||||
pool_lp_auth.is_authorized = true;
|
||||
|
||||
ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![pool_lp_auth, AccountForTests::lp_lock_holding_uninit()],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: MINIMUM_LIQUIDITY,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
|
||||
IdForTests::pool_definition_id(),
|
||||
)])
|
||||
}
|
||||
|
||||
fn cc_new_definition_token_lp_user() -> ChainedCall {
|
||||
ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
AccountForTests::pool_lp_reinitialized_after_lock(),
|
||||
AccountForTests::user_holding_lp_uninit(),
|
||||
],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: BalanceForTests::lp_supply_init(),
|
||||
amount_to_mint: BalanceForTests::lp_user_init(),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
|
||||
@ -374,6 +432,10 @@ impl IdForTests {
|
||||
compute_liquidity_token_pda(AMM_PROGRAM_ID, IdForTests::pool_definition_id())
|
||||
}
|
||||
|
||||
fn lp_lock_holding_id() -> AccountId {
|
||||
compute_lp_lock_holding_pda(AMM_PROGRAM_ID, IdForTests::pool_definition_id())
|
||||
}
|
||||
|
||||
fn user_token_a_id() -> AccountId {
|
||||
AccountId::new([45; 32])
|
||||
}
|
||||
@ -589,6 +651,65 @@ impl AccountWithMetadataForTests {
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_lp_reinitializable() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: TOKEN_PROGRAM_ID,
|
||||
balance: 0u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 0,
|
||||
metadata_id: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::token_lp_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_lp_reinitialized_after_lock() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: TOKEN_PROGRAM_ID,
|
||||
balance: 0u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: MINIMUM_LIQUIDITY,
|
||||
metadata_id: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::token_lp_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_lp_uninit() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::token_lp_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_lp_created_after_lock() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: TOKEN_PROGRAM_ID,
|
||||
balance: 0u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("LP Token"),
|
||||
total_supply: MINIMUM_LIQUIDITY,
|
||||
metadata_id: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::token_lp_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_lp_with_wrong_id() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
@ -623,13 +744,17 @@ impl AccountWithMetadataForTests {
|
||||
}
|
||||
|
||||
fn user_holding_lp_init() -> AccountWithMetadata {
|
||||
AccountForTests::user_holding_lp_with_balance(BalanceForTests::user_token_lp_balance())
|
||||
}
|
||||
|
||||
fn user_holding_lp_with_balance(balance: u128) -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: TOKEN_PROGRAM_ID,
|
||||
balance: 0u128,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: IdForTests::token_lp_definition_id(),
|
||||
balance: BalanceForTests::user_token_lp_balance(),
|
||||
balance,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
@ -638,6 +763,22 @@ impl AccountWithMetadataForTests {
|
||||
}
|
||||
}
|
||||
|
||||
fn lp_lock_holding_uninit() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: false,
|
||||
account_id: IdForTests::lp_lock_holding_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lp_lock_holding_with_wrong_id() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: false,
|
||||
account_id: IdForTests::vault_a_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_definition_init() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
@ -841,7 +982,7 @@ impl AccountWithMetadataForTests {
|
||||
vault_a_id: IdForTests::vault_a_id(),
|
||||
vault_b_id: IdForTests::vault_b_id(),
|
||||
liquidity_pool_id: IdForTests::token_lp_definition_id(),
|
||||
liquidity_pool_supply: 989,
|
||||
liquidity_pool_supply: BalanceForTests::add_lp_supply_successful(),
|
||||
reserve_a: BalanceForTests::vault_a_add_successful(),
|
||||
reserve_b: BalanceForTests::vault_b_add_successful(),
|
||||
fees: 0u128,
|
||||
@ -865,7 +1006,7 @@ impl AccountWithMetadataForTests {
|
||||
vault_a_id: IdForTests::vault_a_id(),
|
||||
vault_b_id: IdForTests::vault_b_id(),
|
||||
liquidity_pool_id: IdForTests::token_lp_definition_id(),
|
||||
liquidity_pool_supply: 607,
|
||||
liquidity_pool_supply: BalanceForTests::remove_lp_supply_successful(),
|
||||
reserve_a: BalanceForTests::vault_a_remove_successful(),
|
||||
reserve_b: BalanceForTests::vault_b_remove_successful(),
|
||||
fees: 0u128,
|
||||
@ -902,6 +1043,30 @@ impl AccountWithMetadataForTests {
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_definition_reinitializable() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: ProgramId::default(),
|
||||
balance: 0u128,
|
||||
data: Data::from(&PoolDefinition {
|
||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||
vault_a_id: IdForTests::vault_a_id(),
|
||||
vault_b_id: IdForTests::vault_b_id(),
|
||||
liquidity_pool_id: IdForTests::token_lp_definition_id(),
|
||||
liquidity_pool_supply: 0,
|
||||
reserve_a: 0,
|
||||
reserve_b: 0,
|
||||
fees: 0u128,
|
||||
active: false,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_definition_with_wrong_id() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
@ -981,6 +1146,32 @@ impl AccountWithMetadataForTests {
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy/corrupted pool state whose reported supply has already been drained down to the
|
||||
/// permanent lock (liquidity_pool_supply == MINIMUM_LIQUIDITY).
|
||||
fn pool_definition_at_minimum_liquidity() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: ProgramId::default(),
|
||||
balance: 0u128,
|
||||
data: Data::from(&PoolDefinition {
|
||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||
vault_a_id: IdForTests::vault_a_id(),
|
||||
vault_b_id: IdForTests::vault_b_id(),
|
||||
liquidity_pool_id: IdForTests::token_lp_definition_id(),
|
||||
liquidity_pool_supply: MINIMUM_LIQUIDITY,
|
||||
reserve_a: BalanceForTests::vault_a_reserve_init(),
|
||||
reserve_b: BalanceForTests::vault_b_reserve_init(),
|
||||
fees: 0u128,
|
||||
active: true,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1322,6 +1513,44 @@ fn test_call_remove_liquidity_insufficient_balance_1() {
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Pool only contains locked liquidity")]
|
||||
#[test]
|
||||
fn test_call_remove_liquidity_pool_at_minimum_liquidity() {
|
||||
// Removing from a legacy/corrupted pool that is already at the locked floor must be rejected.
|
||||
let _post_states = remove_liquidity(
|
||||
AccountWithMetadataForTests::pool_definition_at_minimum_liquidity(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_with_balance(MINIMUM_LIQUIDITY),
|
||||
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
|
||||
1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Cannot remove locked minimum liquidity")]
|
||||
#[test]
|
||||
fn test_call_remove_liquidity_exceeds_unlocked_supply() {
|
||||
// Model corrupted ownership by giving the caller the full LP supply even though the lock
|
||||
// account should permanently hold MINIMUM_LIQUIDITY. The guard must still refuse to burn
|
||||
// through the permanent floor.
|
||||
let _post_states = remove_liquidity(
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_with_balance(BalanceForTests::lp_supply_init()),
|
||||
NonZero::new(BalanceForTests::lp_supply_init()).unwrap(),
|
||||
1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(
|
||||
expected = "Insufficient minimal withdraw amount (Token B) provided for liquidity amount"
|
||||
)]
|
||||
@ -1414,6 +1643,7 @@ fn test_call_new_definition_with_zero_balance_1() {
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1431,6 +1661,7 @@ fn test_call_new_definition_with_zero_balance_2() {
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1448,6 +1679,7 @@ fn test_call_new_definition_same_token_definition() {
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1465,6 +1697,25 @@ fn test_call_new_definition_wrong_liquidity_id() {
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_with_wrong_id(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
|
||||
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "LP lock holding Account ID does not match PDA")]
|
||||
#[test]
|
||||
fn test_call_new_definition_wrong_lp_lock_holding_id() {
|
||||
let _post_states = new_definition(
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_with_wrong_id(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1482,6 +1733,7 @@ fn test_call_new_definition_wrong_pool_id() {
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1499,6 +1751,7 @@ fn test_call_new_definition_wrong_vault_id_1() {
|
||||
AccountWithMetadataForTests::vault_a_with_wrong_id(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1516,6 +1769,7 @@ fn test_call_new_definition_wrong_vault_id_2() {
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_with_wrong_id(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1533,6 +1787,7 @@ fn test_call_new_definition_cannot_initialize_active_pool() {
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1542,14 +1797,33 @@ fn test_call_new_definition_cannot_initialize_active_pool() {
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Cannot initialize an active Pool Definition")]
|
||||
#[should_panic(expected = "Initial liquidity must exceed minimum liquidity lock")]
|
||||
#[test]
|
||||
fn test_call_new_definition_initial_lp_too_small() {
|
||||
// isqrt(1000 * 1000) = 1000 == MINIMUM_LIQUIDITY, so the assertion fires.
|
||||
let _post_states = new_definition(
|
||||
AccountWithMetadataForTests::pool_definition_reinitializable(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_reinitializable(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
|
||||
NonZero::new(MINIMUM_LIQUIDITY).unwrap(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_new_definition_chained_call_successful() {
|
||||
let (post_states, chained_calls) = new_definition(
|
||||
AccountWithMetadataForTests::pool_definition_active(),
|
||||
AccountWithMetadataForTests::pool_definition_reinitializable(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::pool_lp_reinitializable(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1560,18 +1834,17 @@ fn test_call_new_definition_chained_call_successful() {
|
||||
|
||||
let pool_post = post_states[0].clone();
|
||||
|
||||
assert!(
|
||||
AccountWithMetadataForTests::pool_definition_add_successful().account
|
||||
== *pool_post.account()
|
||||
);
|
||||
assert!(AccountWithMetadataForTests::pool_definition_init().account == *pool_post.account());
|
||||
|
||||
let chained_call_lp = chained_calls[0].clone();
|
||||
let chained_call_b = chained_calls[1].clone();
|
||||
let chained_call_a = chained_calls[2].clone();
|
||||
let chained_call_lp_lock = chained_calls[0].clone();
|
||||
let chained_call_lp_user = chained_calls[1].clone();
|
||||
let chained_call_b = chained_calls[2].clone();
|
||||
let chained_call_a = chained_calls[3].clone();
|
||||
|
||||
assert!(chained_call_a == ChainedCallForTests::cc_new_definition_token_a());
|
||||
assert!(chained_call_b == ChainedCallForTests::cc_new_definition_token_b());
|
||||
assert!(chained_call_lp == ChainedCallForTests::cc_new_definition_token_lp());
|
||||
assert!(chained_call_lp_lock == ChainedCallForTests::cc_new_definition_token_lp_lock());
|
||||
assert!(chained_call_lp_user == ChainedCallForTests::cc_new_definition_token_lp_user());
|
||||
}
|
||||
|
||||
#[should_panic(expected = "AccountId is not a token type for the pool")]
|
||||
@ -1674,7 +1947,7 @@ fn test_call_swap_below_min_out() {
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
BalanceForTests::add_max_amount_a(),
|
||||
BalanceForTests::min_amount_out(),
|
||||
BalanceForTests::min_amount_out_too_high(),
|
||||
IdForTests::token_a_definition_id(),
|
||||
);
|
||||
}
|
||||
@ -1746,10 +2019,11 @@ fn test_call_swap_chained_call_successful_2() {
|
||||
#[test]
|
||||
fn test_new_definition_lp_asymmetric_amounts() {
|
||||
let (post_states, chained_calls) = new_definition(
|
||||
AccountWithMetadataForTests::pool_definition_inactive(),
|
||||
AccountWithMetadataForTests::pool_definition_reinitializable(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::pool_lp_reinitializable(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1766,23 +2040,26 @@ fn test_new_definition_lp_asymmetric_amounts() {
|
||||
BalanceForTests::lp_supply_init()
|
||||
);
|
||||
|
||||
let chained_call_lp = chained_calls[0].clone();
|
||||
assert!(chained_call_lp == ChainedCallForTests::cc_new_definition_token_lp());
|
||||
let chained_call_lp_lock = chained_calls[0].clone();
|
||||
let chained_call_lp_user = chained_calls[1].clone();
|
||||
assert!(chained_call_lp_lock == ChainedCallForTests::cc_new_definition_token_lp_lock());
|
||||
assert!(chained_call_lp_user == ChainedCallForTests::cc_new_definition_token_lp_user());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_definition_lp_symmetric_amounts() {
|
||||
// token_a=100, token_b=100 → LP=sqrt(10_000)=100
|
||||
let token_a_amount = 100u128;
|
||||
let token_b_amount = 100u128;
|
||||
// token_a=2000, token_b=2000 → LP=sqrt(4_000_000)=2000
|
||||
let token_a_amount = 2_000u128;
|
||||
let token_b_amount = 2_000u128;
|
||||
let expected_lp = (token_a_amount * token_b_amount).isqrt();
|
||||
assert_eq!(expected_lp, 100);
|
||||
assert_eq!(expected_lp, 2_000);
|
||||
|
||||
let (post_states, chained_calls) = new_definition(
|
||||
AccountWithMetadataForTests::pool_definition_inactive(),
|
||||
AccountWithMetadataForTests::pool_definition_reinitializable(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::pool_lp_reinitializable(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
@ -1795,20 +2072,167 @@ fn test_new_definition_lp_symmetric_amounts() {
|
||||
let pool_def = PoolDefinition::try_from(&pool_post.account().data).unwrap();
|
||||
assert_eq!(pool_def.liquidity_pool_supply, expected_lp);
|
||||
|
||||
let chained_call_lp = chained_calls[0].clone();
|
||||
let expected_lp_call = ChainedCall::new(
|
||||
let chained_call_lp_lock = chained_calls[0].clone();
|
||||
let chained_call_lp_user = chained_calls[1].clone();
|
||||
|
||||
let mut pool_lp_auth = AccountForTests::pool_lp_reinitializable();
|
||||
pool_lp_auth.is_authorized = true;
|
||||
let expected_lp_lock_call = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
pool_lp_auth.clone(),
|
||||
AccountForTests::lp_lock_holding_uninit(),
|
||||
],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: expected_lp,
|
||||
amount_to_mint: MINIMUM_LIQUIDITY,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
|
||||
IdForTests::pool_definition_id(),
|
||||
)]);
|
||||
|
||||
assert_eq!(chained_call_lp, expected_lp_call);
|
||||
let expected_lp_user_call = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
AccountForTests::pool_lp_reinitialized_after_lock(),
|
||||
AccountForTests::user_holding_lp_uninit(),
|
||||
],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: expected_lp - MINIMUM_LIQUIDITY,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
|
||||
IdForTests::pool_definition_id(),
|
||||
)]);
|
||||
|
||||
assert_eq!(chained_call_lp_lock, expected_lp_lock_call);
|
||||
assert_eq!(chained_call_lp_user, expected_lp_user_call);
|
||||
}
|
||||
|
||||
#[should_panic(
|
||||
expected = "New definition: inactive Pool Definition must have zero LP supply before reinitialization"
|
||||
)]
|
||||
#[test]
|
||||
fn test_call_new_definition_reinitialization_requires_zero_pool_supply() {
|
||||
let _post_states = new_definition(
|
||||
AccountWithMetadataForTests::pool_definition_inactive(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_reinitializable(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
|
||||
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(
|
||||
expected = "New definition: existing LP Token Definition Account must have zero supply before reinitialization"
|
||||
)]
|
||||
#[test]
|
||||
fn test_call_new_definition_reinitialization_requires_zero_lp_definition_supply() {
|
||||
let _post_states = new_definition(
|
||||
AccountWithMetadataForTests::pool_definition_reinitializable(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::lp_lock_holding_uninit(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_uninit(),
|
||||
NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(),
|
||||
NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimum_liquidity_lock_and_remove_all_user_lp() {
|
||||
let pool_uninitialized = AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
};
|
||||
let token_a_amount = BalanceForTests::vault_a_reserve_init();
|
||||
let token_b_amount = BalanceForTests::vault_b_reserve_init();
|
||||
let initial_lp = (token_a_amount * token_b_amount).isqrt();
|
||||
let user_lp = initial_lp - MINIMUM_LIQUIDITY;
|
||||
|
||||
let (post_states, chained_calls) = new_definition(
|
||||
pool_uninitialized,
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::pool_lp_uninit(),
|
||||
AccountForTests::lp_lock_holding_uninit(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
AccountForTests::user_holding_lp_uninit(),
|
||||
NonZero::new(token_a_amount).unwrap(),
|
||||
NonZero::new(token_b_amount).unwrap(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
|
||||
let mut pool_lp_auth = AccountForTests::pool_lp_uninit();
|
||||
pool_lp_auth.is_authorized = true;
|
||||
|
||||
let expected_lock_call = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
pool_lp_auth.clone(),
|
||||
AccountForTests::lp_lock_holding_uninit(),
|
||||
],
|
||||
&token_core::Instruction::NewFungibleDefinition {
|
||||
name: String::from("LP Token"),
|
||||
total_supply: MINIMUM_LIQUIDITY,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
|
||||
IdForTests::pool_definition_id(),
|
||||
)]);
|
||||
let expected_user_call = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
AccountForTests::pool_lp_created_after_lock(),
|
||||
AccountForTests::user_holding_lp_uninit(),
|
||||
],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: user_lp,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(
|
||||
IdForTests::pool_definition_id(),
|
||||
)]);
|
||||
assert_eq!(chained_calls[0], expected_lock_call);
|
||||
assert_eq!(chained_calls[1], expected_user_call);
|
||||
|
||||
let pool_post = PoolDefinition::try_from(&post_states[0].account().data).unwrap();
|
||||
assert_eq!(pool_post.liquidity_pool_supply, initial_lp);
|
||||
|
||||
let pool_for_remove = AccountWithMetadata {
|
||||
account: post_states[0].account().clone(),
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
};
|
||||
let (remove_post_states, _) = remove_liquidity(
|
||||
pool_for_remove,
|
||||
AccountForTests::vault_a_init(),
|
||||
AccountForTests::vault_b_init(),
|
||||
AccountForTests::pool_lp_init(),
|
||||
AccountForTests::user_holding_a(),
|
||||
AccountForTests::user_holding_b(),
|
||||
AccountForTests::user_holding_lp_with_balance(user_lp),
|
||||
NonZero::new(user_lp).unwrap(),
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let pool_after_remove =
|
||||
PoolDefinition::try_from(&remove_post_states[0].account().data).unwrap();
|
||||
assert_eq!(pool_after_remove.liquidity_pool_supply, MINIMUM_LIQUIDITY);
|
||||
assert!(pool_after_remove.reserve_a > 0);
|
||||
assert!(pool_after_remove.reserve_b > 0);
|
||||
assert!(pool_after_remove.active);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use amm_core::PoolDefinition;
|
||||
use amm_core::{PoolDefinition, MINIMUM_LIQUIDITY};
|
||||
use nssa::{
|
||||
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
||||
@ -54,6 +54,10 @@ impl Ids {
|
||||
amm_core::compute_liquidity_token_pda(Self::amm_program(), Self::pool_definition())
|
||||
}
|
||||
|
||||
fn lp_lock_holding() -> AccountId {
|
||||
amm_core::compute_lp_lock_holding_pda(Self::amm_program(), Self::pool_definition())
|
||||
}
|
||||
|
||||
fn vault_a() -> AccountId {
|
||||
amm_core::compute_vault_pda(
|
||||
Self::amm_program(),
|
||||
@ -243,6 +247,10 @@ impl Balances {
|
||||
fn lp_supply_init() -> u128 {
|
||||
(Self::vault_a_init() * Self::vault_b_init()).isqrt()
|
||||
}
|
||||
|
||||
fn lp_user_init() -> u128 {
|
||||
Self::lp_supply_init() - MINIMUM_LIQUIDITY
|
||||
}
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@ -365,6 +373,18 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
fn user_lp_holding_with_balance(balance: u128) -> Account {
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::token_lp_definition(),
|
||||
balance,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Expected post-state accounts ---
|
||||
|
||||
fn pool_definition_swap_1() -> Account {
|
||||
@ -776,7 +796,7 @@ impl Accounts {
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::token_lp_definition(),
|
||||
balance: Balances::lp_supply_init(),
|
||||
balance: Balances::lp_user_init(),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
@ -795,6 +815,18 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
fn lp_lock_holding_new_init() -> Account {
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::token_lp_definition(),
|
||||
balance: MINIMUM_LIQUIDITY,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_definition_new_init() -> Account {
|
||||
Account {
|
||||
program_owner: Ids::amm_program(),
|
||||
@ -946,6 +978,39 @@ fn amm_remove_liquidity() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amm_remove_liquidity_insufficient_user_lp_fails() {
|
||||
let mut state = state_for_amm_tests();
|
||||
state.force_insert_account(Ids::user_lp(), Accounts::user_lp_holding_with_balance(500));
|
||||
|
||||
let instruction = amm_core::Instruction::RemoveLiquidity {
|
||||
remove_liquidity_amount: Balances::remove_lp(),
|
||||
min_amount_to_remove_token_a: Balances::remove_min_a(),
|
||||
min_amount_to_remove_token_b: Balances::remove_min_b(),
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::amm_program(),
|
||||
vec![
|
||||
Ids::pool_definition(),
|
||||
Ids::vault_a(),
|
||||
Ids::vault_b(),
|
||||
Ids::token_lp_definition(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_lp()]);
|
||||
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
assert!(state.transition_from_public_transaction(&tx, 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
|
||||
let mut state = state_for_amm_tests_with_new_def();
|
||||
@ -970,6 +1035,7 @@ fn amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
|
||||
Ids::vault_a(),
|
||||
Ids::vault_b(),
|
||||
Ids::token_lp_definition(),
|
||||
Ids::lp_lock_holding(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
@ -1001,6 +1067,10 @@ fn amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
|
||||
state.get_account_by_id(Ids::token_lp_definition()),
|
||||
Accounts::token_lp_definition_new_init()
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::lp_lock_holding()),
|
||||
Accounts::lp_lock_holding_new_init()
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::user_a()),
|
||||
Accounts::user_a_holding_new_init()
|
||||
@ -1040,6 +1110,7 @@ fn amm_new_definition_inactive_initialized_pool_init_user_lp() {
|
||||
Ids::vault_a(),
|
||||
Ids::vault_b(),
|
||||
Ids::token_lp_definition(),
|
||||
Ids::lp_lock_holding(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
@ -1071,6 +1142,10 @@ fn amm_new_definition_inactive_initialized_pool_init_user_lp() {
|
||||
state.get_account_by_id(Ids::token_lp_definition()),
|
||||
Accounts::token_lp_definition_new_init()
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::lp_lock_holding()),
|
||||
Accounts::lp_lock_holding_new_init()
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::user_a()),
|
||||
Accounts::user_a_holding_new_init()
|
||||
@ -1104,6 +1179,7 @@ fn amm_new_definition_uninitialized_pool() {
|
||||
Ids::vault_a(),
|
||||
Ids::vault_b(),
|
||||
Ids::token_lp_definition(),
|
||||
Ids::lp_lock_holding(),
|
||||
Ids::user_a(),
|
||||
Ids::user_b(),
|
||||
Ids::user_lp(),
|
||||
@ -1135,6 +1211,10 @@ fn amm_new_definition_uninitialized_pool() {
|
||||
state.get_account_by_id(Ids::token_lp_definition()),
|
||||
Accounts::token_lp_definition_new_init()
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::lp_lock_holding()),
|
||||
Accounts::lp_lock_holding_new_init()
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::user_a()),
|
||||
Accounts::user_a_holding_new_init()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user