Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 10 additions & 28 deletions amm/src/add.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::num::NonZeroU128;

use amm_core::{assert_supported_fee_tier, compute_liquidity_token_pda_seed, PoolDefinition};
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},
Expand Down Expand Up @@ -44,33 +47,9 @@ pub fn add_liquidity(
"Both max-balances must be nonzero"
);

// 2. Determine deposit amount
let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
.expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault B");
let token_core::TokenHolding::Fungible {
definition_id: _,
balance: vault_b_balance,
} = vault_b_token_holding
else {
panic!(
"Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault B"
);
};

let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
.expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault A");
let token_core::TokenHolding::Fungible {
definition_id: _,
balance: vault_a_balance,
} = vault_a_token_holding
else {
panic!(
"Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault A"
);
};
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LP calculation should be done on the reserves only (assuming they are up-to-date)
"Not up-to-date" means there's been a donation in the meantime. In which case, one can call syncReserves first.

We might also want to look into syncing reserves on any state changing operation to reduce likelihood of surplus.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the main spec mismatch in the PR.

Issue #19 and the parent issue #7 explicitly describe a surplus-based model: the fee stays in the vault above tracked reserves, remove-liquidity pays from actual vault balances, and add-liquidity should use live vault balances after fee accrual so new LPs do not inherit previously earned fees. If we want reserve-only math instead, I think we should rewrite the issue/acceptance criteria first and review that design change explicitly.

Do you want me to do that?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can write the issue if you want. @fryorcraken has there been a verdict on whether or not we allow for recover surplus? My understanding was that we don't want/need it, so any surplus can be synced to the pool reserves (which benefits liquidity providers).

LPs are then always calculated based on reserves.
If a surplus has been created before and the LP doesn't call sync before removing, then it would be on them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest no recover surplus, in line with your expectatiojns: logos-co/rfp#41

let (vault_a_balance, vault_b_balance) =
read_vault_fungible_balances("Add liquidity", &vault_a, &vault_b);

assert!(pool_def_data.reserve_a != 0, "Reserves must be nonzero");
assert!(pool_def_data.reserve_b != 0, "Reserves must be nonzero");
assert!(
vault_a_balance >= pool_def_data.reserve_a,
"Vaults' balances must be at least the reserve amounts"
Expand All @@ -80,7 +59,10 @@ pub fn add_liquidity(
"Vaults' balances must be at least the reserve amounts"
);
Comment thread
0x-r4bbit marked this conversation as resolved.
Comment thread
0x-r4bbit marked this conversation as resolved.

// Calculate actual_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)
Expand Down
81 changes: 48 additions & 33 deletions amm/src/swap.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use amm_core::{assert_supported_fee_tier, MINIMUM_LIQUIDITY};
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},
Expand Down Expand Up @@ -28,31 +30,13 @@ fn validate_swap_setup(
"Vault B was not provided"
);

let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
.expect("AMM Program expects a valid Token Holding Account for Vault A");
let token_core::TokenHolding::Fungible {
definition_id: _,
balance: vault_a_balance,
} = vault_a_token_holding
else {
panic!("AMM Program expects a valid Fungible Token Holding Account for Vault A");
};
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"
);

let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
.expect("AMM Program expects a valid Token Holding Account for Vault B");
let token_core::TokenHolding::Fungible {
definition_id: _,
balance: vault_b_balance,
} = vault_b_token_holding
else {
panic!("AMM Program expects a valid Fungible Token Holding Account for Vault B");
};

assert!(
vault_b_balance >= pool_def_data.reserve_b,
"Reserve for Token B exceeds vault balance"
Expand Down Expand Up @@ -130,6 +114,7 @@ pub fn swap_exact_input(
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,
Expand All @@ -144,6 +129,7 @@ pub fn swap_exact_input(
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,
Expand Down Expand Up @@ -178,19 +164,29 @@ fn swap_logic(
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) {
// Compute withdraw amount
// Maintains pool constant product
// k = pool_def_data.reserve_a * pool_def_data.reserve_b;
let effective_amount_in = swap_amount_in
.checked_mul(FEE_BPS_DENOMINATOR - fee_bps)
.expect("swap_amount_in * (FEE_BPS_DENOMINATOR - fee_bps) overflows u128")
/ FEE_BPS_DENOMINATOR;
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(swap_amount_in)
.expect("reserve * amount_in overflows u128")
.checked_mul(effective_amount_in)
.expect("reserve * effective_amount_in overflows u128")
/ reserve_deposit_vault_amount
.checked_add(swap_amount_in)
.expect("reserve + swap_amount_in overflows u128");
.checked_add(effective_amount_in)
.expect("reserve + effective_amount_in overflows u128");

// Slippage check
assert!(
Expand Down Expand Up @@ -259,6 +255,7 @@ pub fn swap_exact_output(
max_amount_in,
pool_def_data.reserve_a,
pool_def_data.reserve_b,
pool_def_data.fees,
pool.account_id,
);

Expand All @@ -273,6 +270,7 @@ pub fn swap_exact_output(
max_amount_in,
pool_def_data.reserve_b,
pool_def_data.reserve_a,
pool_def_data.fees,
pool.account_id,
);

Expand Down Expand Up @@ -307,6 +305,7 @@ fn exact_output_swap_logic(
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
Expand All @@ -318,12 +317,28 @@ fn exact_output_swap_logic(
"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
// 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")
.div_ceil(reserve_withdraw_vault_amount - 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!(
Expand Down
Loading
Loading