mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-26 22:51:14 +00:00
refactor: move programs into programs and UIs into apps
This refactors the repository structure as it has grown over time.
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, compute_liquidity_token_pda_seed, read_vault_fungible_balances,
|
||||
PoolDefinition,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
};
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit pool, vault, and user accounts"
|
||||
)]
|
||||
pub fn add_liquidity(
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
pool_definition_lp: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
user_holding_lp: AccountWithMetadata,
|
||||
min_amount_liquidity: NonZeroU128,
|
||||
max_amount_to_add_token_a: u128,
|
||||
max_amount_to_add_token_b: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
// 1. Fetch Pool state
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("Add liquidity: AMM Program expects valid Pool Definition Account");
|
||||
assert_supported_fee_tier(pool_def_data.fees);
|
||||
|
||||
assert_eq!(
|
||||
vault_a.account_id, pool_def_data.vault_a_id,
|
||||
"Vault A was not provided"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
pool_def_data.liquidity_pool_id, pool_definition_lp.account_id,
|
||||
"LP definition mismatch"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
vault_b.account_id, pool_def_data.vault_b_id,
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
let token_program_id = vault_a.account.program_owner;
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the vault's Token Program"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the vault's Token Program"
|
||||
);
|
||||
|
||||
assert!(
|
||||
max_amount_to_add_token_a != 0 && max_amount_to_add_token_b != 0,
|
||||
"Both max-balances must be nonzero"
|
||||
);
|
||||
|
||||
let (vault_a_balance, vault_b_balance) =
|
||||
read_vault_fungible_balances("Add liquidity", &vault_a, &vault_b);
|
||||
|
||||
assert!(
|
||||
vault_a_balance >= pool_def_data.reserve_a,
|
||||
"Vaults' balances must be at least the reserve amounts"
|
||||
);
|
||||
assert!(
|
||||
vault_b_balance >= pool_def_data.reserve_b,
|
||||
"Vaults' balances must be at least the reserve amounts"
|
||||
);
|
||||
|
||||
// 2. Determine deposit amount
|
||||
assert!(pool_def_data.reserve_a != 0, "Reserves must be nonzero");
|
||||
assert!(pool_def_data.reserve_b != 0, "Reserves must be nonzero");
|
||||
|
||||
let ideal_a: u128 = pool_def_data
|
||||
.reserve_a
|
||||
.checked_mul(max_amount_to_add_token_b)
|
||||
.expect("reserve_a * max_amount_b overflows u128")
|
||||
.checked_div(pool_def_data.reserve_b)
|
||||
.expect("reserve_b must be nonzero after validation");
|
||||
let ideal_b: u128 = pool_def_data
|
||||
.reserve_b
|
||||
.checked_mul(max_amount_to_add_token_a)
|
||||
.expect("reserve_b * max_amount_a overflows u128")
|
||||
.checked_div(pool_def_data.reserve_a)
|
||||
.expect("reserve_a must be nonzero after validation");
|
||||
|
||||
let actual_amount_a = if ideal_a > max_amount_to_add_token_a {
|
||||
max_amount_to_add_token_a
|
||||
} else {
|
||||
ideal_a
|
||||
};
|
||||
let actual_amount_b = if ideal_b > max_amount_to_add_token_b {
|
||||
max_amount_to_add_token_b
|
||||
} else {
|
||||
ideal_b
|
||||
};
|
||||
|
||||
// 3. Validate amounts
|
||||
assert!(
|
||||
max_amount_to_add_token_a >= actual_amount_a,
|
||||
"Actual trade amounts cannot exceed max_amounts"
|
||||
);
|
||||
assert!(
|
||||
max_amount_to_add_token_b >= actual_amount_b,
|
||||
"Actual trade amounts cannot exceed max_amounts"
|
||||
);
|
||||
|
||||
assert!(actual_amount_a != 0, "A trade amount is 0");
|
||||
assert!(actual_amount_b != 0, "A trade amount is 0");
|
||||
|
||||
// 4. Calculate LP to mint
|
||||
let delta_lp = std::cmp::min(
|
||||
pool_def_data
|
||||
.liquidity_pool_supply
|
||||
.checked_mul(actual_amount_a)
|
||||
.expect("liquidity_pool_supply * actual_amount_a overflows u128")
|
||||
.checked_div(pool_def_data.reserve_a)
|
||||
.expect("reserve_a must be nonzero after validation"),
|
||||
pool_def_data
|
||||
.liquidity_pool_supply
|
||||
.checked_mul(actual_amount_b)
|
||||
.expect("liquidity_pool_supply * actual_amount_b overflows u128")
|
||||
.checked_div(pool_def_data.reserve_b)
|
||||
.expect("reserve_b must be nonzero after validation"),
|
||||
);
|
||||
|
||||
assert!(delta_lp != 0, "Payable LP must be nonzero");
|
||||
|
||||
assert!(
|
||||
delta_lp >= min_amount_liquidity.get(),
|
||||
"Payable LP is less than provided minimum LP amount"
|
||||
);
|
||||
|
||||
// 5. Update pool account
|
||||
let mut pool_post = pool.account.clone();
|
||||
let pool_post_definition = PoolDefinition {
|
||||
liquidity_pool_supply: pool_def_data
|
||||
.liquidity_pool_supply
|
||||
.checked_add(delta_lp)
|
||||
.expect("liquidity_pool_supply + delta_lp overflows u128"),
|
||||
reserve_a: pool_def_data
|
||||
.reserve_a
|
||||
.checked_add(actual_amount_a)
|
||||
.expect("reserve_a + actual_amount_a overflows u128"),
|
||||
reserve_b: pool_def_data
|
||||
.reserve_b
|
||||
.checked_add(actual_amount_b)
|
||||
.expect("reserve_b + actual_amount_b overflows u128"),
|
||||
..pool_def_data
|
||||
};
|
||||
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
// Chain call for Token A (UserHoldingA -> Vault_A)
|
||||
let call_token_a = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![user_holding_a.clone(), vault_a.clone()],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: actual_amount_a,
|
||||
},
|
||||
);
|
||||
// Chain call for Token B (UserHoldingB -> Vault_B)
|
||||
let call_token_b = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![user_holding_b.clone(), vault_b.clone()],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: actual_amount_b,
|
||||
},
|
||||
);
|
||||
// Chain call for LP (mint new tokens for user_holding_lp)
|
||||
let mut pool_definition_lp_auth = pool_definition_lp.clone();
|
||||
pool_definition_lp_auth.is_authorized = true;
|
||||
let call_token_lp = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![pool_definition_lp_auth.clone(), user_holding_lp.clone()],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: delta_lp,
|
||||
},
|
||||
)
|
||||
.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 post_states = vec![
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
AccountPostState::new(pool_definition_lp.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, chained_calls)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! The AMM Program implementation.
|
||||
|
||||
pub use amm_core as core;
|
||||
|
||||
pub mod add;
|
||||
pub mod new_definition;
|
||||
pub mod remove;
|
||||
pub mod swap;
|
||||
pub mod sync;
|
||||
|
||||
mod tests;
|
||||
@@ -0,0 +1,213 @@
|
||||
use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, compute_liquidity_token_pda, compute_liquidity_token_pda_seed,
|
||||
compute_lp_lock_holding_pda, compute_lp_lock_holding_pda_seed, compute_pool_pda,
|
||||
compute_pool_pda_seed, compute_vault_pda, compute_vault_pda_seed, PoolDefinition,
|
||||
MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, Claim, ProgramId},
|
||||
};
|
||||
use token_core::TokenDefinition;
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit pool, vault, mint, lock, and user accounts"
|
||||
)]
|
||||
pub fn new_definition(
|
||||
pool: AccountWithMetadata,
|
||||
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,
|
||||
token_a_amount: NonZeroU128,
|
||||
token_b_amount: NonZeroU128,
|
||||
fees: u128,
|
||||
amm_program_id: ProgramId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let definition_token_a_id = token_core::TokenHolding::try_from(&user_holding_a.account.data)
|
||||
.expect("New definition: AMM Program expects valid Token Holding account for Token A")
|
||||
.definition_id();
|
||||
let definition_token_b_id = token_core::TokenHolding::try_from(&user_holding_b.account.data)
|
||||
.expect("New definition: AMM Program expects valid Token Holding account for Token B")
|
||||
.definition_id();
|
||||
|
||||
let token_program = user_holding_a.account.program_owner;
|
||||
|
||||
// both instances of the same token program
|
||||
assert_eq!(
|
||||
user_holding_b.account.program_owner, token_program,
|
||||
"User Token holdings must use the same Token Program"
|
||||
);
|
||||
// Verify token_a and token_b are different
|
||||
assert!(
|
||||
definition_token_a_id != definition_token_b_id,
|
||||
"Cannot set up a swap for a token with itself"
|
||||
);
|
||||
assert_eq!(
|
||||
pool.account_id,
|
||||
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id),
|
||||
"Pool Definition Account ID does not match PDA"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_a.account_id,
|
||||
compute_vault_pda(amm_program_id, pool.account_id, definition_token_a_id),
|
||||
"Vault ID does not match PDA"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_b.account_id,
|
||||
compute_vault_pda(amm_program_id, pool.account_id, definition_token_b_id),
|
||||
"Vault ID does not match PDA"
|
||||
);
|
||||
assert_eq!(
|
||||
pool_definition_lp.account_id,
|
||||
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"
|
||||
);
|
||||
assert_supported_fee_tier(fees);
|
||||
|
||||
// Assert that pool is uninitialized (hard precondition)
|
||||
assert_eq!(
|
||||
pool.account,
|
||||
Account::default(),
|
||||
"Pool account must be uninitialized"
|
||||
);
|
||||
assert!(
|
||||
user_holding_lp.account != Account::default() || user_holding_lp.is_authorized,
|
||||
"Fresh user LP holding requires user authorization"
|
||||
);
|
||||
|
||||
// LP Token minting calculation
|
||||
let initial_lp = token_a_amount
|
||||
.get()
|
||||
.checked_mul(token_b_amount.get())
|
||||
.expect("token_a * token_b overflows u128")
|
||||
.isqrt();
|
||||
assert!(
|
||||
initial_lp > MINIMUM_LIQUIDITY,
|
||||
"Initial liquidity must exceed minimum liquidity lock"
|
||||
);
|
||||
let user_lp = initial_lp
|
||||
.checked_sub(MINIMUM_LIQUIDITY)
|
||||
.expect("initial liquidity must exceed minimum liquidity after validation");
|
||||
|
||||
// Update pool account
|
||||
let mut pool_post = pool.account.clone();
|
||||
let pool_post_definition = PoolDefinition {
|
||||
definition_token_a_id,
|
||||
definition_token_b_id,
|
||||
vault_a_id: vault_a.account_id,
|
||||
vault_b_id: vault_b.account_id,
|
||||
liquidity_pool_id: pool_definition_lp.account_id,
|
||||
liquidity_pool_supply: initial_lp,
|
||||
reserve_a: token_a_amount.into(),
|
||||
reserve_b: token_b_amount.into(),
|
||||
fees,
|
||||
};
|
||||
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
let pool_post: AccountPostState = AccountPostState::new_claimed(
|
||||
pool_post.clone(),
|
||||
Claim::Pda(compute_pool_pda_seed(
|
||||
definition_token_a_id,
|
||||
definition_token_b_id,
|
||||
)),
|
||||
);
|
||||
|
||||
let token_program_id = user_holding_a.account.program_owner;
|
||||
|
||||
// Chain call for Token A (user_holding_a -> Vault_A)
|
||||
let mut vault_a_authorized = vault_a.clone();
|
||||
vault_a_authorized.is_authorized = true;
|
||||
let call_token_a = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![user_holding_a.clone(), vault_a_authorized],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: token_a_amount.into(),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||
pool.account_id,
|
||||
definition_token_a_id,
|
||||
)]);
|
||||
// Chain call for Token B (user_holding_b -> Vault_B)
|
||||
let mut vault_b_authorized = vault_b.clone();
|
||||
vault_b_authorized.is_authorized = true;
|
||||
let call_token_b = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![user_holding_b.clone(), vault_b_authorized],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: token_b_amount.into(),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||
pool.account_id,
|
||||
definition_token_b_id,
|
||||
)]);
|
||||
|
||||
// Chain call for liquidity token lock holding
|
||||
let mut pool_lp_auth = pool_definition_lp.clone();
|
||||
pool_lp_auth.is_authorized = true;
|
||||
let mut lp_lock_holding_auth = lp_lock_holding.clone();
|
||||
lp_lock_holding_auth.is_authorized = true;
|
||||
|
||||
let call_token_lp_lock = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![pool_lp_auth.clone(), lp_lock_holding_auth],
|
||||
&token_core::Instruction::NewFungibleDefinition {
|
||||
name: String::from("LP Token"),
|
||||
total_supply: MINIMUM_LIQUIDITY,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![
|
||||
compute_liquidity_token_pda_seed(pool.account_id),
|
||||
compute_lp_lock_holding_pda_seed(pool.account_id),
|
||||
]);
|
||||
|
||||
let mut pool_lp_after_lock = pool_lp_auth.clone();
|
||||
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,
|
||||
});
|
||||
|
||||
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, chained_calls)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, compute_liquidity_token_pda_seed, compute_vault_pda_seed,
|
||||
PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
};
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit pool, vault, and user accounts"
|
||||
)]
|
||||
pub fn remove_liquidity(
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
pool_definition_lp: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
user_holding_lp: AccountWithMetadata,
|
||||
remove_liquidity_amount: NonZeroU128,
|
||||
min_amount_to_remove_token_a: u128,
|
||||
min_amount_to_remove_token_b: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let remove_liquidity_amount: u128 = remove_liquidity_amount.into();
|
||||
|
||||
// 1. Fetch Pool state
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("Remove liquidity: AMM Program expects a valid Pool Definition Account");
|
||||
assert_supported_fee_tier(pool_def_data.fees);
|
||||
|
||||
assert!(
|
||||
pool_def_data.liquidity_pool_supply >= MINIMUM_LIQUIDITY,
|
||||
"Pool liquidity supply is below minimum liquidity"
|
||||
);
|
||||
assert_eq!(
|
||||
pool_def_data.liquidity_pool_id, pool_definition_lp.account_id,
|
||||
"LP definition mismatch"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_a.account_id, pool_def_data.vault_a_id,
|
||||
"Vault A was not provided"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_b.account_id, pool_def_data.vault_b_id,
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
let token_program_id = vault_a.account.program_owner;
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the vault's Token Program"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the vault's Token Program"
|
||||
);
|
||||
|
||||
// Vault addresses do not need to be checked with PDA
|
||||
// calculation for setting authorization since stored
|
||||
// in the Pool Definition.
|
||||
let mut running_vault_a = vault_a.clone();
|
||||
let mut running_vault_b = vault_b.clone();
|
||||
running_vault_a.is_authorized = true;
|
||||
running_vault_b.is_authorized = true;
|
||||
|
||||
assert!(
|
||||
min_amount_to_remove_token_a != 0,
|
||||
"Minimum withdraw amount must be nonzero"
|
||||
);
|
||||
assert!(
|
||||
min_amount_to_remove_token_b != 0,
|
||||
"Minimum withdraw amount must be nonzero"
|
||||
);
|
||||
|
||||
// 2. Compute withdrawal amounts
|
||||
let user_holding_lp_data = token_core::TokenHolding::try_from(&user_holding_lp.account.data)
|
||||
.expect("Remove liquidity: AMM Program expects a valid Token Account for liquidity token");
|
||||
let token_core::TokenHolding::Fungible {
|
||||
definition_id: _,
|
||||
balance: user_lp_balance,
|
||||
} = user_holding_lp_data
|
||||
else {
|
||||
panic!(
|
||||
"Remove liquidity: AMM Program expects a valid Fungible Token Holding Account for liquidity token"
|
||||
);
|
||||
};
|
||||
|
||||
assert!(
|
||||
user_lp_balance <= pool_def_data.liquidity_pool_supply,
|
||||
"Invalid liquidity account provided"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_lp_data.definition_id(),
|
||||
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"
|
||||
);
|
||||
assert!(
|
||||
remove_liquidity_amount <= user_lp_balance,
|
||||
"Remove amount exceeds user LP balance"
|
||||
);
|
||||
let unlocked_liquidity = pool_def_data
|
||||
.liquidity_pool_supply
|
||||
.checked_sub(MINIMUM_LIQUIDITY)
|
||||
.expect("liquidity supply must be at least the locked minimum after validation");
|
||||
// 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
|
||||
.checked_mul(remove_liquidity_amount)
|
||||
.expect("reserve_a * remove_liquidity_amount overflows u128")
|
||||
.checked_div(pool_def_data.liquidity_pool_supply)
|
||||
.expect("liquidity supply must be nonzero after validation");
|
||||
let withdraw_amount_b = pool_def_data
|
||||
.reserve_b
|
||||
.checked_mul(remove_liquidity_amount)
|
||||
.expect("reserve_b * remove_liquidity_amount overflows u128")
|
||||
.checked_div(pool_def_data.liquidity_pool_supply)
|
||||
.expect("liquidity supply must be nonzero after validation");
|
||||
|
||||
// 3. Validate and slippage check
|
||||
assert!(
|
||||
withdraw_amount_a >= min_amount_to_remove_token_a,
|
||||
"Insufficient minimal withdraw amount (Token A) provided for liquidity amount"
|
||||
);
|
||||
assert!(
|
||||
withdraw_amount_b >= min_amount_to_remove_token_b,
|
||||
"Insufficient minimal withdraw amount (Token B) provided for liquidity amount"
|
||||
);
|
||||
|
||||
// 4. Calculate LP to reduce cap by
|
||||
let delta_lp: u128 = remove_liquidity_amount;
|
||||
|
||||
// 5. Update pool account
|
||||
let mut pool_post = pool.account.clone();
|
||||
let pool_post_definition = PoolDefinition {
|
||||
liquidity_pool_supply: pool_def_data
|
||||
.liquidity_pool_supply
|
||||
.checked_sub(delta_lp)
|
||||
.expect("liquidity_pool_supply - delta_lp underflows"),
|
||||
reserve_a: pool_def_data
|
||||
.reserve_a
|
||||
.checked_sub(withdraw_amount_a)
|
||||
.expect("reserve_a - withdraw_amount_a underflows"),
|
||||
reserve_b: pool_def_data
|
||||
.reserve_b
|
||||
.checked_sub(withdraw_amount_b)
|
||||
.expect("reserve_b - withdraw_amount_b underflows"),
|
||||
..pool_def_data.clone()
|
||||
};
|
||||
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
// Chaincall for Token A withdraw
|
||||
let call_token_a = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![running_vault_a, user_holding_a.clone()],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: withdraw_amount_a,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||
pool.account_id,
|
||||
pool_def_data.definition_token_a_id,
|
||||
)]);
|
||||
// Chaincall for Token B withdraw
|
||||
let call_token_b = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![running_vault_b, user_holding_b.clone()],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: withdraw_amount_b,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||
pool.account_id,
|
||||
pool_def_data.definition_token_b_id,
|
||||
)]);
|
||||
// Chaincall for LP adjustment
|
||||
let mut pool_definition_lp_auth = pool_definition_lp.clone();
|
||||
pool_definition_lp_auth.is_authorized = true;
|
||||
let call_token_lp = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![pool_definition_lp_auth, user_holding_lp.clone()],
|
||||
&token_core::Instruction::Burn {
|
||||
amount_to_burn: delta_lp,
|
||||
},
|
||||
)
|
||||
.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 post_states = vec![
|
||||
AccountPostState::new(pool_post.clone()),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
AccountPostState::new(pool_definition_lp.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, chained_calls)
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, read_vault_fungible_balances, FEE_BPS_DENOMINATOR, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
pub use amm_core::{compute_liquidity_token_pda_seed, compute_vault_pda_seed, PoolDefinition};
|
||||
use nssa_core::{
|
||||
account::{AccountId, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
};
|
||||
|
||||
/// Validates swap setup: checks pool liquidity is ready, vaults match, and reserves are sufficient.
|
||||
fn validate_swap_setup(
|
||||
pool: &AccountWithMetadata,
|
||||
vault_a: &AccountWithMetadata,
|
||||
vault_b: &AccountWithMetadata,
|
||||
) -> PoolDefinition {
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("AMM Program expects a valid Pool Definition Account");
|
||||
assert_supported_fee_tier(pool_def_data.fees);
|
||||
|
||||
assert!(
|
||||
pool_def_data.liquidity_pool_supply >= MINIMUM_LIQUIDITY,
|
||||
"Pool liquidity supply is below minimum liquidity"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_a.account_id, pool_def_data.vault_a_id,
|
||||
"Vault A was not provided"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_b.account_id, pool_def_data.vault_b_id,
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
let (vault_a_balance, vault_b_balance) =
|
||||
read_vault_fungible_balances("Validate swap setup", vault_a, vault_b);
|
||||
|
||||
assert!(
|
||||
vault_a_balance >= pool_def_data.reserve_a,
|
||||
"Reserve for Token A exceeds vault balance"
|
||||
);
|
||||
assert!(
|
||||
vault_b_balance >= pool_def_data.reserve_b,
|
||||
"Reserve for Token B exceeds vault balance"
|
||||
);
|
||||
|
||||
pool_def_data
|
||||
}
|
||||
|
||||
/// Creates post-state and returns reserves after swap.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "post-state assembly keeps pool, vault, user account, and delta state explicit"
|
||||
)]
|
||||
#[expect(
|
||||
clippy::needless_pass_by_value,
|
||||
reason = "consistent with codebase style"
|
||||
)]
|
||||
fn create_swap_post_states(
|
||||
pool: AccountWithMetadata,
|
||||
pool_def_data: PoolDefinition,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
deposit_a: u128,
|
||||
withdraw_a: u128,
|
||||
deposit_b: u128,
|
||||
withdraw_b: u128,
|
||||
) -> Vec<AccountPostState> {
|
||||
let mut pool_post = pool.account;
|
||||
let pool_post_definition = PoolDefinition {
|
||||
reserve_a: pool_def_data
|
||||
.reserve_a
|
||||
.checked_add(deposit_a)
|
||||
.expect("reserve_a + deposit_a overflows u128")
|
||||
.checked_sub(withdraw_a)
|
||||
.expect("reserve_a + deposit_a - withdraw_a underflows"),
|
||||
reserve_b: pool_def_data
|
||||
.reserve_b
|
||||
.checked_add(deposit_b)
|
||||
.expect("reserve_b + deposit_b overflows u128")
|
||||
.checked_sub(withdraw_b)
|
||||
.expect("reserve_b + deposit_b - withdraw_b underflows"),
|
||||
..pool_def_data
|
||||
};
|
||||
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
vec![
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account),
|
||||
AccountPostState::new(vault_b.account),
|
||||
AccountPostState::new(user_holding_a.account),
|
||||
AccountPostState::new(user_holding_b.account),
|
||||
]
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit pool, vault, and user accounts"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn swap_exact_input(
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
swap_amount_in: u128,
|
||||
min_amount_out: u128,
|
||||
token_in_id: AccountId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
|
||||
|
||||
let token_program_id = vault_a.account.program_owner;
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the vault's Token Program"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the vault's Token Program"
|
||||
);
|
||||
|
||||
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||
if token_in_id == pool_def_data.definition_token_a_id {
|
||||
let (chained_calls, deposit_a, withdraw_b) = swap_logic(
|
||||
user_holding_a.clone(),
|
||||
vault_a.clone(),
|
||||
vault_b.clone(),
|
||||
user_holding_b.clone(),
|
||||
swap_amount_in,
|
||||
min_amount_out,
|
||||
pool_def_data.fees,
|
||||
pool_def_data.reserve_a,
|
||||
pool_def_data.reserve_b,
|
||||
pool.account_id,
|
||||
);
|
||||
|
||||
(chained_calls, [deposit_a, 0], [0, withdraw_b])
|
||||
} else if token_in_id == pool_def_data.definition_token_b_id {
|
||||
let (chained_calls, deposit_b, withdraw_a) = swap_logic(
|
||||
user_holding_b.clone(),
|
||||
vault_b.clone(),
|
||||
vault_a.clone(),
|
||||
user_holding_a.clone(),
|
||||
swap_amount_in,
|
||||
min_amount_out,
|
||||
pool_def_data.fees,
|
||||
pool_def_data.reserve_b,
|
||||
pool_def_data.reserve_a,
|
||||
pool.account_id,
|
||||
);
|
||||
|
||||
(chained_calls, [0, withdraw_a], [deposit_b, 0])
|
||||
} else {
|
||||
panic!("AccountId is not a token type for the pool");
|
||||
};
|
||||
|
||||
let post_states = create_swap_post_states(
|
||||
pool,
|
||||
pool_def_data,
|
||||
vault_a,
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
deposit_a,
|
||||
withdraw_a,
|
||||
deposit_b,
|
||||
withdraw_b,
|
||||
);
|
||||
|
||||
(post_states, chained_calls)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "swap calculation keeps account context and pricing parameters explicit"
|
||||
)]
|
||||
fn swap_logic(
|
||||
user_deposit: AccountWithMetadata,
|
||||
vault_deposit: AccountWithMetadata,
|
||||
vault_withdraw: AccountWithMetadata,
|
||||
user_withdraw: AccountWithMetadata,
|
||||
swap_amount_in: u128,
|
||||
min_amount_out: u128,
|
||||
fee_bps: u128,
|
||||
reserve_deposit_vault_amount: u128,
|
||||
reserve_withdraw_vault_amount: u128,
|
||||
pool_id: AccountId,
|
||||
) -> (Vec<ChainedCall>, u128, u128) {
|
||||
let fee_multiplier = FEE_BPS_DENOMINATOR
|
||||
.checked_sub(fee_bps)
|
||||
.expect("fee_bps exceeds fee denominator");
|
||||
let effective_amount_in = swap_amount_in
|
||||
.checked_mul(fee_multiplier)
|
||||
.expect("swap_amount_in * (FEE_BPS_DENOMINATOR - fee_bps) overflows u128")
|
||||
.checked_div(FEE_BPS_DENOMINATOR)
|
||||
.expect("fee denominator must be nonzero");
|
||||
assert!(
|
||||
effective_amount_in != 0,
|
||||
"Effective swap amount should be nonzero"
|
||||
);
|
||||
// Compute the withdraw amount using the fee-adjusted input for pricing.
|
||||
// The recorded pool reserves are updated later with the full
|
||||
// `swap_amount_in`, so LP fees accrue inside `reserve_*` via invariant
|
||||
// growth rather than as a separate vault balance surplus over `reserve_*`.
|
||||
let withdraw_amount = reserve_withdraw_vault_amount
|
||||
.checked_mul(effective_amount_in)
|
||||
.expect("reserve * effective_amount_in overflows u128")
|
||||
.checked_div(
|
||||
reserve_deposit_vault_amount
|
||||
.checked_add(effective_amount_in)
|
||||
.expect("reserve + effective_amount_in overflows u128"),
|
||||
)
|
||||
.expect("reserve plus effective input must be nonzero");
|
||||
|
||||
// Slippage check
|
||||
assert!(
|
||||
min_amount_out <= withdraw_amount,
|
||||
"Withdraw amount is less than minimal amount out"
|
||||
);
|
||||
assert!(withdraw_amount != 0, "Withdraw amount should be nonzero");
|
||||
|
||||
let token_program_id = user_deposit.account.program_owner;
|
||||
|
||||
let mut chained_calls = Vec::new();
|
||||
chained_calls.push(ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![user_deposit, vault_deposit],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: swap_amount_in,
|
||||
},
|
||||
));
|
||||
|
||||
let mut vault_withdraw = vault_withdraw.clone();
|
||||
vault_withdraw.is_authorized = true;
|
||||
|
||||
let pda_seed = compute_vault_pda_seed(
|
||||
pool_id,
|
||||
token_core::TokenHolding::try_from(&vault_withdraw.account.data)
|
||||
.expect("Swap Logic: AMM Program expects valid token data")
|
||||
.definition_id(),
|
||||
);
|
||||
|
||||
chained_calls.push(
|
||||
ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![vault_withdraw, user_withdraw],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: withdraw_amount,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![pda_seed]),
|
||||
);
|
||||
|
||||
(chained_calls, swap_amount_in, withdraw_amount)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit pool, vault, and user accounts"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn swap_exact_output(
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
user_holding_a: AccountWithMetadata,
|
||||
user_holding_b: AccountWithMetadata,
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
token_in_id: AccountId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
|
||||
|
||||
let token_program_id = vault_a.account.program_owner;
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the vault's Token Program"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the vault's Token Program"
|
||||
);
|
||||
|
||||
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||
if token_in_id == pool_def_data.definition_token_a_id {
|
||||
let (chained_calls, deposit_a, withdraw_b) = exact_output_swap_logic(
|
||||
user_holding_a.clone(),
|
||||
vault_a.clone(),
|
||||
vault_b.clone(),
|
||||
user_holding_b.clone(),
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
pool_def_data.reserve_a,
|
||||
pool_def_data.reserve_b,
|
||||
pool_def_data.fees,
|
||||
pool.account_id,
|
||||
);
|
||||
|
||||
(chained_calls, [deposit_a, 0], [0, withdraw_b])
|
||||
} else if token_in_id == pool_def_data.definition_token_b_id {
|
||||
let (chained_calls, deposit_b, withdraw_a) = exact_output_swap_logic(
|
||||
user_holding_b.clone(),
|
||||
vault_b.clone(),
|
||||
vault_a.clone(),
|
||||
user_holding_a.clone(),
|
||||
exact_amount_out,
|
||||
max_amount_in,
|
||||
pool_def_data.reserve_b,
|
||||
pool_def_data.reserve_a,
|
||||
pool_def_data.fees,
|
||||
pool.account_id,
|
||||
);
|
||||
|
||||
(chained_calls, [0, withdraw_a], [deposit_b, 0])
|
||||
} else {
|
||||
panic!("AccountId is not a token type for the pool");
|
||||
};
|
||||
|
||||
let post_states = create_swap_post_states(
|
||||
pool,
|
||||
pool_def_data,
|
||||
vault_a,
|
||||
vault_b,
|
||||
user_holding_a,
|
||||
user_holding_b,
|
||||
deposit_a,
|
||||
withdraw_a,
|
||||
deposit_b,
|
||||
withdraw_b,
|
||||
);
|
||||
|
||||
(post_states, chained_calls)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "swap calculation keeps account context and pricing parameters explicit"
|
||||
)]
|
||||
fn exact_output_swap_logic(
|
||||
user_deposit: AccountWithMetadata,
|
||||
vault_deposit: AccountWithMetadata,
|
||||
vault_withdraw: AccountWithMetadata,
|
||||
user_withdraw: AccountWithMetadata,
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
reserve_deposit_vault_amount: u128,
|
||||
reserve_withdraw_vault_amount: u128,
|
||||
fee_bps: u128,
|
||||
pool_id: AccountId,
|
||||
) -> (Vec<ChainedCall>, u128, u128) {
|
||||
// Guard: exact_amount_out must be nonzero
|
||||
assert_ne!(exact_amount_out, 0, "Exact amount out must be nonzero");
|
||||
|
||||
// Guard: exact_amount_out must be less than reserve_withdraw_vault_amount
|
||||
assert!(
|
||||
exact_amount_out < reserve_withdraw_vault_amount,
|
||||
"Exact amount out exceeds reserve"
|
||||
);
|
||||
|
||||
// Compute the minimum effective input required to achieve exact_amount_out
|
||||
// using the same floor-rounded fee application as swap_exact_input.
|
||||
//
|
||||
// Solve constant product for effective_in (fee already removed):
|
||||
// effective_in >= ceil(reserve_in * amount_out / (reserve_out - amount_out))
|
||||
let effective_in_numerator = reserve_deposit_vault_amount
|
||||
.checked_mul(exact_amount_out)
|
||||
.expect("reserve * amount_out overflows u128");
|
||||
let effective_in_denominator = reserve_withdraw_vault_amount
|
||||
.checked_sub(exact_amount_out)
|
||||
.expect("reserve_out - amount_out underflows");
|
||||
let effective_in_min = effective_in_numerator.div_ceil(effective_in_denominator);
|
||||
|
||||
// Lift back to gross input so that
|
||||
// floor(gross_in * (FEE_DENOM - fee) / FEE_DENOM) >= effective_in_min
|
||||
let fee_multiplier = FEE_BPS_DENOMINATOR
|
||||
.checked_sub(fee_bps)
|
||||
.expect("fee_bps exceeds fee denominator");
|
||||
let deposit_amount = effective_in_min
|
||||
.checked_mul(FEE_BPS_DENOMINATOR)
|
||||
.expect("effective_in * FEE_DENOM overflows u128")
|
||||
.div_ceil(fee_multiplier);
|
||||
|
||||
// Slippage check
|
||||
assert!(
|
||||
deposit_amount <= max_amount_in,
|
||||
"Required input exceeds maximum amount in"
|
||||
);
|
||||
|
||||
let token_program_id = user_deposit.account.program_owner;
|
||||
|
||||
let mut chained_calls = Vec::new();
|
||||
chained_calls.push(ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![user_deposit, vault_deposit],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: deposit_amount,
|
||||
},
|
||||
));
|
||||
|
||||
let mut vault_withdraw = vault_withdraw;
|
||||
vault_withdraw.is_authorized = true;
|
||||
|
||||
let pda_seed = compute_vault_pda_seed(
|
||||
pool_id,
|
||||
token_core::TokenHolding::try_from(&vault_withdraw.account.data)
|
||||
.expect("Exact Output Swap Logic: AMM Program expects valid token data")
|
||||
.definition_id(),
|
||||
);
|
||||
|
||||
chained_calls.push(
|
||||
ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![vault_withdraw, user_withdraw],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: exact_amount_out,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![pda_seed]),
|
||||
);
|
||||
|
||||
(chained_calls, deposit_amount, exact_amount_out)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, read_vault_fungible_balances, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
};
|
||||
|
||||
pub fn sync_reserves(
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("Sync reserves: AMM Program expects a valid Pool Definition Account");
|
||||
assert_supported_fee_tier(pool_def_data.fees);
|
||||
|
||||
assert!(
|
||||
pool_def_data.liquidity_pool_supply >= MINIMUM_LIQUIDITY,
|
||||
"Pool liquidity supply is below minimum liquidity"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_a.account_id, pool_def_data.vault_a_id,
|
||||
"Vault A was not provided"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_b.account_id, pool_def_data.vault_b_id,
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
let (vault_a_balance, vault_b_balance) =
|
||||
read_vault_fungible_balances("Sync reserves", &vault_a, &vault_b);
|
||||
assert!(
|
||||
vault_a_balance >= pool_def_data.reserve_a,
|
||||
"Sync reserves: vault A balance is less than its reserve"
|
||||
);
|
||||
assert!(
|
||||
vault_b_balance >= pool_def_data.reserve_b,
|
||||
"Sync reserves: vault B balance is less than its reserve"
|
||||
);
|
||||
|
||||
let mut pool_post = pool.account.clone();
|
||||
let pool_post_definition = PoolDefinition {
|
||||
reserve_a: vault_a_balance,
|
||||
reserve_b: vault_b_balance,
|
||||
..pool_def_data
|
||||
};
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
(
|
||||
vec![
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
],
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user