2026-03-17 18:08:53 +01:00
|
|
|
use std::num::NonZeroU128;
|
|
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
use amm_core::{
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda_seed,
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
compute_pool_pda_seed, compute_vault_pda_seed, spot_price_q64_64, AmmConfig, PoolDefinition,
|
|
|
|
|
MINIMUM_LIQUIDITY,
|
2026-04-08 17:48:13 -03:00
|
|
|
};
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
2026-03-17 18:08:53 +01:00
|
|
|
use nssa_core::{
|
|
|
|
|
account::{AccountWithMetadata, Data},
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
program::{AccountPostState, ChainedCall, ProgramId},
|
2026-03-17 18:08:53 +01:00
|
|
|
};
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
use twap_oracle_core::compute_current_tick_account_pda;
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-05-06 17:08:15 -03:00
|
|
|
#[expect(
|
|
|
|
|
clippy::too_many_arguments,
|
|
|
|
|
reason = "instruction surface passes explicit pool, vault, and user accounts"
|
|
|
|
|
)]
|
2026-03-17 18:08:53 +01:00
|
|
|
pub fn remove_liquidity(
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
config: AccountWithMetadata,
|
2026-03-17 18:08:53 +01:00
|
|
|
pool: AccountWithMetadata,
|
|
|
|
|
vault_a: AccountWithMetadata,
|
|
|
|
|
vault_b: AccountWithMetadata,
|
|
|
|
|
pool_definition_lp: AccountWithMetadata,
|
|
|
|
|
user_holding_a: AccountWithMetadata,
|
|
|
|
|
user_holding_b: AccountWithMetadata,
|
|
|
|
|
user_holding_lp: AccountWithMetadata,
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
current_tick_account: AccountWithMetadata,
|
|
|
|
|
clock: AccountWithMetadata,
|
2026-03-17 18:08:53 +01:00
|
|
|
remove_liquidity_amount: NonZeroU128,
|
|
|
|
|
min_amount_to_remove_token_a: u128,
|
|
|
|
|
min_amount_to_remove_token_b: u128,
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
amm_program_id: ProgramId,
|
2026-03-17 18:08:53 +01:00
|
|
|
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
|
|
|
|
let remove_liquidity_amount: u128 = remove_liquidity_amount.into();
|
|
|
|
|
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
// The program IDs are taken from the config account, not trusted from a caller-supplied
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
// holding. Validating the config PDA is also the Program's initialization gate.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
config.account_id,
|
|
|
|
|
compute_config_pda(amm_program_id),
|
|
|
|
|
"Remove liquidity: AMM config Account ID does not match PDA"
|
|
|
|
|
);
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
let config_data = AmmConfig::try_from(&config.account.data)
|
|
|
|
|
.expect("Remove liquidity: AMM Program must be initialized before use");
|
|
|
|
|
let token_program_id = config_data.token_program_id;
|
|
|
|
|
let twap_oracle_program_id = config_data.twap_oracle_program_id;
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
// 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");
|
2026-03-31 20:45:57 -03:00
|
|
|
assert_supported_fee_tier(pool_def_data.fees);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-04-10 15:43:13 -03:00
|
|
|
assert!(
|
|
|
|
|
pool_def_data.liquidity_pool_supply >= MINIMUM_LIQUIDITY,
|
|
|
|
|
"Pool liquidity supply is below minimum liquidity"
|
|
|
|
|
);
|
2026-03-17 18:08:53 +01:00
|
|
|
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"
|
|
|
|
|
);
|
|
|
|
|
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
assert_eq!(
|
|
|
|
|
vault_a.account.program_owner, token_program_id,
|
|
|
|
|
"Vault A must be owned by the configured Token Program"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
vault_b.account.program_owner, token_program_id,
|
|
|
|
|
"Vault B must be owned by the configured Token Program"
|
|
|
|
|
);
|
2026-04-28 12:48:53 +02:00
|
|
|
assert_eq!(
|
|
|
|
|
user_holding_a.account.program_owner, token_program_id,
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
"User Token A holding must be owned by the configured Token Program"
|
2026-04-28 12:48:53 +02:00
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
user_holding_b.account.program_owner, token_program_id,
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
"User Token B holding must be owned by the configured Token Program"
|
2026-04-28 12:48:53 +02:00
|
|
|
);
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
// The current tick is refreshed by a chained call to the oracle; validate its PDA and the
|
|
|
|
|
// clock here so the removal is rejected early with an AMM-level error.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
|
|
|
|
"Remove liquidity: clock account must be the canonical 1-block LEZ clock account"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
current_tick_account.account_id,
|
|
|
|
|
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
|
|
|
|
|
"Remove liquidity: current tick Account ID does not match PDA"
|
|
|
|
|
);
|
2026-04-28 12:48:53 +02:00
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
// 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"
|
|
|
|
|
);
|
2026-04-08 17:48:13 -03:00
|
|
|
// 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"
|
|
|
|
|
);
|
2026-05-04 14:47:50 +02:00
|
|
|
assert!(
|
|
|
|
|
remove_liquidity_amount <= user_lp_balance,
|
|
|
|
|
"Remove amount exceeds user LP balance"
|
|
|
|
|
);
|
2026-05-06 17:08:15 -03:00
|
|
|
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");
|
2026-04-08 17:48:13 -03:00
|
|
|
// 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"
|
|
|
|
|
);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-04-07 10:38:14 +02:00
|
|
|
let withdraw_amount_a = pool_def_data
|
|
|
|
|
.reserve_a
|
|
|
|
|
.checked_mul(remove_liquidity_amount)
|
|
|
|
|
.expect("reserve_a * remove_liquidity_amount overflows u128")
|
2026-05-06 17:08:15 -03:00
|
|
|
.checked_div(pool_def_data.liquidity_pool_supply)
|
|
|
|
|
.expect("liquidity supply must be nonzero after validation");
|
2026-04-07 10:38:14 +02:00
|
|
|
let withdraw_amount_b = pool_def_data
|
|
|
|
|
.reserve_b
|
|
|
|
|
.checked_mul(remove_liquidity_amount)
|
|
|
|
|
.expect("reserve_b * remove_liquidity_amount overflows u128")
|
2026-05-06 17:08:15 -03:00
|
|
|
.checked_div(pool_def_data.liquidity_pool_supply)
|
|
|
|
|
.expect("liquidity supply must be nonzero after validation");
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
// 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
|
2026-04-08 17:48:13 -03:00
|
|
|
let delta_lp: u128 = remove_liquidity_amount;
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
// 5. Update pool account
|
|
|
|
|
let mut pool_post = pool.account.clone();
|
|
|
|
|
let pool_post_definition = PoolDefinition {
|
2026-04-07 10:38:14 +02:00
|
|
|
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"),
|
2026-03-17 18:08:53 +01:00
|
|
|
..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)]);
|
|
|
|
|
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
// Refresh the pool's TWAP current tick from the post-removal spot price. The pool is already
|
|
|
|
|
// owned by this program, so it is passed (in its post-removal state) as the authorized price
|
|
|
|
|
// source.
|
|
|
|
|
let new_price = spot_price_q64_64(
|
|
|
|
|
pool_post_definition.reserve_a,
|
|
|
|
|
pool_post_definition.reserve_b,
|
|
|
|
|
);
|
|
|
|
|
let pool_price_source = AccountWithMetadata {
|
|
|
|
|
account: pool_post.clone(),
|
|
|
|
|
is_authorized: true,
|
|
|
|
|
account_id: pool.account_id,
|
|
|
|
|
};
|
|
|
|
|
let call_update_tick = ChainedCall::new(
|
|
|
|
|
twap_oracle_program_id,
|
|
|
|
|
vec![
|
|
|
|
|
current_tick_account.clone(),
|
|
|
|
|
pool_price_source,
|
|
|
|
|
clock.clone(),
|
|
|
|
|
],
|
|
|
|
|
&twap_oracle_core::Instruction::UpdateCurrentTick { price: new_price },
|
|
|
|
|
)
|
|
|
|
|
.with_pda_seeds(vec![compute_pool_pda_seed(
|
|
|
|
|
pool_def_data.definition_token_a_id,
|
|
|
|
|
pool_def_data.definition_token_b_id,
|
|
|
|
|
)]);
|
|
|
|
|
|
|
|
|
|
let chained_calls = vec![call_token_lp, call_token_b, call_token_a, call_update_tick];
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
let post_states = vec![
|
feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.
The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.
- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-17 16:29:30 +02:00
|
|
|
AccountPostState::new(config.account.clone()),
|
2026-03-17 18:08:53 +01:00
|
|
|
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()),
|
feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.
sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.
The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-18 15:50:11 +02:00
|
|
|
AccountPostState::new(current_tick_account.account.clone()),
|
|
|
|
|
AccountPostState::new(clock.account.clone()),
|
2026-03-17 18:08:53 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
(post_states, chained_calls)
|
|
|
|
|
}
|