mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-05-19 07:29:32 +00:00
feat(amm): add swap exact output instruction
Adds SwapExactOutput to the AMM, allowing callers to specify the exact desired output amount while the protocol computes the required input (ceiling division to prevent rounding in the protocol's favour). The swap-exact-output success tests now use a dedicated small-pool fixture (reserve_a=1_000, reserve_b=500) rather than the shared pool_definition_init, which had its reserves bumped to 5_000/2_500 in a later commit to satisfy the MINIMUM_LIQUIDITY invariant introduced for new_definition. Using a dedicated fixture keeps each test self-contained and avoids hardcoded expected values silently breaking when shared baselines change.
This commit is contained in:
parent
e61cd594b5
commit
8aa3c8190a
@ -240,6 +240,55 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "swap_exact_output",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "pool",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vault_a",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vault_b",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_holding_a",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_holding_b",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "exact_amount_out",
|
||||||
|
"type": "u128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_amount_in",
|
||||||
|
"type": "u128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "token_definition_id_in",
|
||||||
|
"type": "account_id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "sync_reserves",
|
"name": "sync_reserves",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
|
|||||||
@ -84,6 +84,22 @@ pub enum Instruction {
|
|||||||
token_definition_id_in: AccountId,
|
token_definition_id_in: AccountId,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Swap tokens specifying the exact desired output amount,
|
||||||
|
/// while maintaining the Pool constant product.
|
||||||
|
///
|
||||||
|
/// Required accounts:
|
||||||
|
/// - AMM Pool (initialized)
|
||||||
|
/// - Vault Holding Account for Token A (initialized)
|
||||||
|
/// - Vault Holding Account for Token B (initialized)
|
||||||
|
/// - User Holding Account for Token A
|
||||||
|
/// - User Holding Account for Token B Either User Holding Account for Token A or Token B is
|
||||||
|
/// authorized.
|
||||||
|
SwapExactOutput {
|
||||||
|
exact_amount_out: u128,
|
||||||
|
max_amount_in: u128,
|
||||||
|
token_definition_id_in: AccountId,
|
||||||
|
},
|
||||||
|
|
||||||
/// Sync pool reserves with current vault balances.
|
/// Sync pool reserves with current vault balances.
|
||||||
///
|
///
|
||||||
/// Required accounts:
|
/// Required accounts:
|
||||||
|
|||||||
@ -130,6 +130,31 @@ mod amm {
|
|||||||
Ok(SpelOutput::with_chained_calls(post_states, chained_calls))
|
Ok(SpelOutput::with_chained_calls(post_states, chained_calls))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Swap tokens specifying the exact desired output amount.
|
||||||
|
#[instruction]
|
||||||
|
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_definition_id_in: AccountId,
|
||||||
|
) -> SpelResult {
|
||||||
|
let (post_states, chained_calls) = amm_program::swap::swap_exact_output(
|
||||||
|
pool,
|
||||||
|
vault_a,
|
||||||
|
vault_b,
|
||||||
|
user_holding_a,
|
||||||
|
user_holding_b,
|
||||||
|
exact_amount_out,
|
||||||
|
max_amount_in,
|
||||||
|
token_definition_id_in,
|
||||||
|
);
|
||||||
|
Ok(SpelOutput::with_chained_calls(post_states, chained_calls))
|
||||||
|
}
|
||||||
|
|
||||||
/// Sync pool reserves with current vault balances.
|
/// Sync pool reserves with current vault balances.
|
||||||
#[instruction]
|
#[instruction]
|
||||||
pub fn sync_reserves(
|
pub fn sync_reserves(
|
||||||
|
|||||||
248
amm/src/swap.rs
248
amm/src/swap.rs
@ -4,20 +4,14 @@ use nssa_core::{
|
|||||||
program::{AccountPostState, ChainedCall},
|
program::{AccountPostState, ChainedCall},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
/// Validates swap setup: checks pool is active, vaults match, and reserves are sufficient.
|
||||||
pub fn swap(
|
fn validate_swap_setup(
|
||||||
pool: AccountWithMetadata,
|
pool: &AccountWithMetadata,
|
||||||
vault_a: AccountWithMetadata,
|
vault_a: &AccountWithMetadata,
|
||||||
vault_b: AccountWithMetadata,
|
vault_b: &AccountWithMetadata,
|
||||||
user_holding_a: AccountWithMetadata,
|
) -> PoolDefinition {
|
||||||
user_holding_b: AccountWithMetadata,
|
|
||||||
swap_amount_in: u128,
|
|
||||||
min_amount_out: u128,
|
|
||||||
token_in_id: AccountId,
|
|
||||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
|
||||||
// Verify vaults are in fact vaults
|
|
||||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||||
.expect("Swap: AMM Program expects a valid Pool Definition Account");
|
.expect("AMM Program expects a valid Pool Definition Account");
|
||||||
|
|
||||||
assert!(pool_def_data.active, "Pool is inactive");
|
assert!(pool_def_data.active, "Pool is inactive");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -29,16 +23,14 @@ pub fn swap(
|
|||||||
"Vault B was not provided"
|
"Vault B was not provided"
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetch pool reserves
|
|
||||||
// validates reserves is at least the vaults' balances
|
|
||||||
let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
|
let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
|
||||||
.expect("Swap: AMM Program expects a valid Token Holding Account for Vault A");
|
.expect("AMM Program expects a valid Token Holding Account for Vault A");
|
||||||
let token_core::TokenHolding::Fungible {
|
let token_core::TokenHolding::Fungible {
|
||||||
definition_id: _,
|
definition_id: _,
|
||||||
balance: vault_a_balance,
|
balance: vault_a_balance,
|
||||||
} = vault_a_token_holding
|
} = vault_a_token_holding
|
||||||
else {
|
else {
|
||||||
panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A");
|
panic!("AMM Program expects a valid Fungible Token Holding Account for Vault A");
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@ -47,13 +39,13 @@ pub fn swap(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
|
let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
|
||||||
.expect("Swap: AMM Program expects a valid Token Holding Account for Vault B");
|
.expect("AMM Program expects a valid Token Holding Account for Vault B");
|
||||||
let token_core::TokenHolding::Fungible {
|
let token_core::TokenHolding::Fungible {
|
||||||
definition_id: _,
|
definition_id: _,
|
||||||
balance: vault_b_balance,
|
balance: vault_b_balance,
|
||||||
} = vault_b_token_holding
|
} = vault_b_token_holding
|
||||||
else {
|
else {
|
||||||
panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B");
|
panic!("AMM Program expects a valid Fungible Token Holding Account for Vault B");
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@ -61,6 +53,59 @@ pub fn swap(
|
|||||||
"Reserve for Token B exceeds vault balance"
|
"Reserve for Token B exceeds vault balance"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
pool_def_data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates post-state and returns reserves after swap.
|
||||||
|
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||||
|
#[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 + deposit_a - withdraw_a,
|
||||||
|
reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b,
|
||||||
|
..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 = "TODO: Fix later")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn swap(
|
||||||
|
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 (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||||
if token_in_id == pool_def_data.definition_token_a_id {
|
if token_in_id == pool_def_data.definition_token_a_id {
|
||||||
let (chained_calls, deposit_a, withdraw_b) = swap_logic(
|
let (chained_calls, deposit_a, withdraw_b) = swap_logic(
|
||||||
@ -94,23 +139,18 @@ pub fn swap(
|
|||||||
panic!("AccountId is not a token type for the pool");
|
panic!("AccountId is not a token type for the pool");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update pool account
|
let post_states = create_swap_post_states(
|
||||||
let mut pool_post = pool.account.clone();
|
pool,
|
||||||
let pool_post_definition = PoolDefinition {
|
pool_def_data,
|
||||||
reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a,
|
vault_a,
|
||||||
reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b,
|
vault_b,
|
||||||
..pool_def_data
|
user_holding_a,
|
||||||
};
|
user_holding_b,
|
||||||
|
deposit_a,
|
||||||
pool_post.data = Data::from(&pool_post_definition);
|
withdraw_a,
|
||||||
|
deposit_b,
|
||||||
let post_states = vec![
|
withdraw_b,
|
||||||
AccountPostState::new(pool_post.clone()),
|
);
|
||||||
AccountPostState::new(vault_a.account.clone()),
|
|
||||||
AccountPostState::new(vault_b.account.clone()),
|
|
||||||
AccountPostState::new(user_holding_a.account.clone()),
|
|
||||||
AccountPostState::new(user_holding_b.account.clone()),
|
|
||||||
];
|
|
||||||
|
|
||||||
(post_states, chained_calls)
|
(post_states, chained_calls)
|
||||||
}
|
}
|
||||||
@ -130,7 +170,9 @@ fn swap_logic(
|
|||||||
// Compute withdraw amount
|
// Compute withdraw amount
|
||||||
// Maintains pool constant product
|
// Maintains pool constant product
|
||||||
// k = pool_def_data.reserve_a * pool_def_data.reserve_b;
|
// k = pool_def_data.reserve_a * pool_def_data.reserve_b;
|
||||||
let withdraw_amount = (reserve_withdraw_vault_amount * swap_amount_in)
|
let withdraw_amount = reserve_withdraw_vault_amount
|
||||||
|
.checked_mul(swap_amount_in)
|
||||||
|
.expect("reserve * amount_in overflows u128")
|
||||||
/ (reserve_deposit_vault_amount + swap_amount_in);
|
/ (reserve_deposit_vault_amount + swap_amount_in);
|
||||||
|
|
||||||
// Slippage check
|
// Slippage check
|
||||||
@ -174,3 +216,135 @@ fn swap_logic(
|
|||||||
|
|
||||||
(chained_calls, swap_amount_in, withdraw_amount)
|
(chained_calls, swap_amount_in, withdraw_amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||||
|
#[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 (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.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.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 = "TODO: Fix later")]
|
||||||
|
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,
|
||||||
|
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 deposit amount using ceiling division
|
||||||
|
// Formula: amount_in = ceil(reserve_in * exact_amount_out / (reserve_out - exact_amount_out))
|
||||||
|
let deposit_amount = reserve_deposit_vault_amount
|
||||||
|
.checked_mul(exact_amount_out)
|
||||||
|
.expect("reserve * amount_out overflows u128")
|
||||||
|
.div_ceil(reserve_withdraw_vault_amount - exact_amount_out);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|||||||
428
amm/src/tests.rs
428
amm/src/tests.rs
@ -13,7 +13,10 @@ use nssa_core::{
|
|||||||
use token_core::{TokenDefinition, TokenHolding};
|
use token_core::{TokenDefinition, TokenHolding};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
add::add_liquidity, new_definition::new_definition, remove::remove_liquidity, swap::swap,
|
add::add_liquidity,
|
||||||
|
new_definition::new_definition,
|
||||||
|
remove::remove_liquidity,
|
||||||
|
swap::{swap, swap_exact_output},
|
||||||
sync::sync_reserves,
|
sync::sync_reserves,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,6 +160,10 @@ impl BalanceForTests {
|
|||||||
BalanceForTests::add_max_amount_b()
|
BalanceForTests::add_max_amount_b()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn max_amount_in() -> u128 {
|
||||||
|
166
|
||||||
|
}
|
||||||
|
|
||||||
fn vault_a_remove_successful() -> u128 {
|
fn vault_a_remove_successful() -> u128 {
|
||||||
BalanceForTests::vault_a_reserve_init() - BalanceForTests::remove_actual_a_successful()
|
BalanceForTests::vault_a_reserve_init() - BalanceForTests::remove_actual_a_successful()
|
||||||
}
|
}
|
||||||
@ -263,6 +270,74 @@ impl ChainedCallForTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cc_swap_exact_output_token_a_test_1() -> ChainedCall {
|
||||||
|
let swap_amount: u128 = 498;
|
||||||
|
|
||||||
|
ChainedCall::new(
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
vec![
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: swap_amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cc_swap_exact_output_token_b_test_1() -> ChainedCall {
|
||||||
|
let swap_amount: u128 = 166;
|
||||||
|
|
||||||
|
let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init();
|
||||||
|
vault_b_auth.is_authorized = true;
|
||||||
|
|
||||||
|
ChainedCall::new(
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
vec![vault_b_auth, AccountWithMetadataForTests::user_holding_b()],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: swap_amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||||
|
IdForTests::pool_definition_id(),
|
||||||
|
IdForTests::token_b_definition_id(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cc_swap_exact_output_token_a_test_2() -> ChainedCall {
|
||||||
|
let swap_amount: u128 = 285;
|
||||||
|
|
||||||
|
let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init();
|
||||||
|
vault_a_auth.is_authorized = true;
|
||||||
|
|
||||||
|
ChainedCall::new(
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
vec![vault_a_auth, AccountWithMetadataForTests::user_holding_a()],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: swap_amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||||
|
IdForTests::pool_definition_id(),
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cc_swap_exact_output_token_b_test_2() -> ChainedCall {
|
||||||
|
let swap_amount: u128 = 200;
|
||||||
|
|
||||||
|
ChainedCall::new(
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
vec![
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: swap_amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn cc_add_token_a() -> ChainedCall {
|
fn cc_add_token_a() -> ChainedCall {
|
||||||
ChainedCall::new(
|
ChainedCall::new(
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
@ -804,6 +879,34 @@ impl AccountWithMetadataForTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A smaller pool (reserve_a=1_000, reserve_b=500) used exclusively by
|
||||||
|
/// swap-exact-output tests, whose hardcoded expected values were computed
|
||||||
|
/// against these reserves. vault_a_init/vault_b_init still satisfy the
|
||||||
|
/// balance ≥ reserve check (5_000 ≥ 1_000, 2_500 ≥ 500).
|
||||||
|
fn pool_definition_swap_exact_output_init() -> 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: BalanceForTests::lp_supply_init(),
|
||||||
|
reserve_a: 1_000,
|
||||||
|
reserve_b: 500,
|
||||||
|
fees: 0u128,
|
||||||
|
active: true,
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: true,
|
||||||
|
account_id: IdForTests::pool_definition_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn pool_definition_init_reserve_a_zero() -> AccountWithMetadata {
|
fn pool_definition_init_reserve_a_zero() -> AccountWithMetadata {
|
||||||
AccountWithMetadata {
|
AccountWithMetadata {
|
||||||
account: Account {
|
account: Account {
|
||||||
@ -948,6 +1051,54 @@ impl AccountWithMetadataForTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pool_definition_swap_exact_output_test_1() -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: ProgramId::default(),
|
||||||
|
balance: 0_u128,
|
||||||
|
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: BalanceForTests::lp_supply_init(),
|
||||||
|
reserve_a: 1498_u128,
|
||||||
|
reserve_b: 334_u128,
|
||||||
|
fees: 0_u128,
|
||||||
|
active: true,
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: true,
|
||||||
|
account_id: IdForTests::pool_definition_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pool_definition_swap_exact_output_test_2() -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: ProgramId::default(),
|
||||||
|
balance: 0_u128,
|
||||||
|
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: BalanceForTests::lp_supply_init(),
|
||||||
|
reserve_a: 715_u128,
|
||||||
|
reserve_b: 700_u128,
|
||||||
|
fees: 0_u128,
|
||||||
|
active: true,
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: true,
|
||||||
|
account_id: IdForTests::pool_definition_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn pool_definition_add_zero_lp() -> AccountWithMetadata {
|
fn pool_definition_add_zero_lp() -> AccountWithMetadata {
|
||||||
AccountWithMetadata {
|
AccountWithMetadata {
|
||||||
account: Account {
|
account: Account {
|
||||||
@ -2017,6 +2168,281 @@ fn test_call_swap_chained_call_successful_2() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "AccountId is not a token type for the pool")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_incorrect_token_type() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
BalanceForTests::add_max_amount_a(),
|
||||||
|
BalanceForTests::max_amount_in(),
|
||||||
|
IdForTests::token_lp_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "Vault A was not provided")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_vault_a_omitted() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_with_wrong_id(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
BalanceForTests::add_max_amount_a(),
|
||||||
|
BalanceForTests::max_amount_in(),
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "Vault B was not provided")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_vault_b_omitted() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_with_wrong_id(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
BalanceForTests::add_max_amount_a(),
|
||||||
|
BalanceForTests::max_amount_in(),
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "Reserve for Token A exceeds vault balance")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_reserves_vault_mismatch_1() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init_low(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
BalanceForTests::add_max_amount_a(),
|
||||||
|
BalanceForTests::max_amount_in(),
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "Reserve for Token B exceeds vault balance")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_reserves_vault_mismatch_2() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init_low(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
BalanceForTests::add_max_amount_a(),
|
||||||
|
BalanceForTests::max_amount_in(),
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "Pool is inactive")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_inactive() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_inactive(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
BalanceForTests::add_max_amount_a(),
|
||||||
|
BalanceForTests::max_amount_in(),
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "Required input exceeds maximum amount in")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_exceeds_max_in() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
166_u128,
|
||||||
|
100_u128,
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "Exact amount out must be nonzero")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_zero() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
0_u128,
|
||||||
|
500_u128,
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[should_panic(expected = "Exact amount out exceeds reserve")]
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_exceeds_reserve() {
|
||||||
|
let _post_states = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
BalanceForTests::vault_b_reserve_init(),
|
||||||
|
BalanceForTests::max_amount_in(),
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_chained_call_successful() {
|
||||||
|
let (post_states, chained_calls) = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_swap_exact_output_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
BalanceForTests::max_amount_in(),
|
||||||
|
BalanceForTests::vault_b_reserve_init(),
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let pool_post = post_states[0].clone();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
AccountWithMetadataForTests::pool_definition_swap_exact_output_test_1().account
|
||||||
|
== *pool_post.account()
|
||||||
|
);
|
||||||
|
|
||||||
|
let chained_call_a = chained_calls[0].clone();
|
||||||
|
let chained_call_b = chained_calls[1].clone();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chained_call_a,
|
||||||
|
ChainedCallForTests::cc_swap_exact_output_token_a_test_1()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
chained_call_b,
|
||||||
|
ChainedCallForTests::cc_swap_exact_output_token_b_test_1()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_swap_exact_output_chained_call_successful_2() {
|
||||||
|
let (post_states, chained_calls) = swap_exact_output(
|
||||||
|
AccountWithMetadataForTests::pool_definition_swap_exact_output_init(),
|
||||||
|
AccountWithMetadataForTests::vault_a_init(),
|
||||||
|
AccountWithMetadataForTests::vault_b_init(),
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
285,
|
||||||
|
300,
|
||||||
|
IdForTests::token_b_definition_id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let pool_post = post_states[0].clone();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
AccountWithMetadataForTests::pool_definition_swap_exact_output_test_2().account
|
||||||
|
== *pool_post.account()
|
||||||
|
);
|
||||||
|
|
||||||
|
let chained_call_a = chained_calls[1].clone();
|
||||||
|
let chained_call_b = chained_calls[0].clone();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chained_call_a,
|
||||||
|
ChainedCallForTests::cc_swap_exact_output_token_a_test_2()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
chained_call_b,
|
||||||
|
ChainedCallForTests::cc_swap_exact_output_token_b_test_2()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without the fix, `reserve_a * exact_amount_out` silently wraps to 0 in release mode,
|
||||||
|
// making `deposit_amount = 0`. The slippage check `0 <= max_amount_in` always passes,
|
||||||
|
// so an attacker receives `exact_amount_out` tokens while paying nothing.
|
||||||
|
#[should_panic(expected = "reserve * amount_out overflows u128")]
|
||||||
|
#[test]
|
||||||
|
fn swap_exact_output_overflow_protection() {
|
||||||
|
// reserve_a chosen so that reserve_a * 2 overflows u128:
|
||||||
|
// (u128::MAX / 2 + 1) * 2 = u128::MAX + 1 → wraps to 0
|
||||||
|
let large_reserve: u128 = u128::MAX / 2 + 1;
|
||||||
|
let reserve_b: u128 = 1_000;
|
||||||
|
|
||||||
|
let pool = AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: ProgramId::default(),
|
||||||
|
balance: 0,
|
||||||
|
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: 1,
|
||||||
|
reserve_a: large_reserve,
|
||||||
|
reserve_b,
|
||||||
|
fees: 0,
|
||||||
|
active: true,
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: true,
|
||||||
|
account_id: IdForTests::pool_definition_id(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let vault_a = AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: TOKEN_PROGRAM_ID,
|
||||||
|
balance: 0,
|
||||||
|
data: Data::from(&TokenHolding::Fungible {
|
||||||
|
definition_id: IdForTests::token_a_definition_id(),
|
||||||
|
balance: large_reserve,
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: true,
|
||||||
|
account_id: IdForTests::vault_a_id(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let vault_b = AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: TOKEN_PROGRAM_ID,
|
||||||
|
balance: 0,
|
||||||
|
data: Data::from(&TokenHolding::Fungible {
|
||||||
|
definition_id: IdForTests::token_b_definition_id(),
|
||||||
|
balance: reserve_b,
|
||||||
|
}),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: true,
|
||||||
|
account_id: IdForTests::vault_b_id(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _result = swap_exact_output(
|
||||||
|
pool,
|
||||||
|
vault_a,
|
||||||
|
vault_b,
|
||||||
|
AccountWithMetadataForTests::user_holding_a(),
|
||||||
|
AccountWithMetadataForTests::user_holding_b(),
|
||||||
|
2, // exact_amount_out: small, valid (< reserve_b)
|
||||||
|
1, // max_amount_in: tiny — real deposit would be enormous, but
|
||||||
|
// overflow wraps it to 0, making 0 <= 1 pass silently
|
||||||
|
IdForTests::token_a_definition_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_definition_lp_asymmetric_amounts() {
|
fn test_new_definition_lp_asymmetric_amounts() {
|
||||||
let (post_states, chained_calls) = new_definition(
|
let (post_states, chained_calls) = new_definition(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user