mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-27 07:01:14 +00:00
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
This commit is contained in:
+27
-6
@@ -1,12 +1,12 @@
|
||||
use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, compute_liquidity_token_pda_seed, read_vault_fungible_balances,
|
||||
PoolDefinition,
|
||||
assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda_seed,
|
||||
read_vault_fungible_balances, AmmConfig, PoolDefinition,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
|
||||
#[expect(
|
||||
@@ -14,6 +14,7 @@ use nssa_core::{
|
||||
reason = "instruction surface passes explicit pool, vault, and user accounts"
|
||||
)]
|
||||
pub fn add_liquidity(
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
@@ -24,7 +25,19 @@ pub fn add_liquidity(
|
||||
min_amount_liquidity: NonZeroU128,
|
||||
max_amount_to_add_token_a: u128,
|
||||
max_amount_to_add_token_b: u128,
|
||||
amm_program_id: ProgramId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// holding. Validating the config PDA is also the Program's initialization gate.
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
compute_config_pda(amm_program_id),
|
||||
"Add liquidity: AMM config Account ID does not match PDA"
|
||||
);
|
||||
let token_program_id = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Add liquidity: AMM Program must be initialized before use")
|
||||
.token_program_id;
|
||||
|
||||
// 1. Fetch Pool state
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("Add liquidity: AMM Program expects valid Pool Definition Account");
|
||||
@@ -45,14 +58,21 @@ pub fn add_liquidity(
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
let token_program_id = vault_a.account.program_owner;
|
||||
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"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the vault's Token Program"
|
||||
"User Token A holding must be owned by the configured 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"
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -187,6 +207,7 @@ pub fn add_liquidity(
|
||||
let chained_calls = vec![call_token_lp, call_token_b, call_token_a];
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(config.account.clone()),
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use amm_core::{compute_config_pda, compute_config_pda_seed, AmmConfig};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, Claim, ProgramId},
|
||||
};
|
||||
|
||||
/// Initializes the AMM Program by creating its singleton configuration account.
|
||||
///
|
||||
/// The config account is a PDA derived from the constant `"CONFIG"` seed
|
||||
/// (`compute_config_pda(amm_program_id)`) and stores `token_program_id`, the Token Program the
|
||||
/// AMM issues every chained call to. Its existence is the Program's "initialized" flag: the
|
||||
/// chained-call instructions read the Token Program ID from it and reject calls until it exists.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if:
|
||||
/// - `config.account_id` does not match `compute_config_pda(amm_program_id)`.
|
||||
/// - `config.account` is not the default (the Program is already initialized).
|
||||
pub fn initialize(
|
||||
config: AccountWithMetadata,
|
||||
token_program_id: ProgramId,
|
||||
amm_program_id: ProgramId,
|
||||
) -> Vec<AccountPostState> {
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
compute_config_pda(amm_program_id),
|
||||
"Initialize: AMM config Account ID does not match PDA"
|
||||
);
|
||||
assert_eq!(
|
||||
config.account,
|
||||
Account::default(),
|
||||
"Initialize: AMM config account must be uninitialized"
|
||||
);
|
||||
|
||||
let mut config_post = config.account.clone();
|
||||
config_post.data = Data::from(&AmmConfig { token_program_id });
|
||||
|
||||
vec![AccountPostState::new_claimed(
|
||||
config_post,
|
||||
Claim::Pda(compute_config_pda_seed()),
|
||||
)]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use amm_core::compute_config_pda;
|
||||
use nssa_core::account::{AccountId, Nonce};
|
||||
|
||||
use super::*;
|
||||
|
||||
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
||||
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||
|
||||
fn config_uninit() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: false,
|
||||
account_id: compute_config_pda(AMM_PROGRAM_ID),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_single_pda_claimed_post_state() {
|
||||
let post_states = initialize(config_uninit(), TOKEN_PROGRAM_ID, AMM_PROGRAM_ID);
|
||||
assert_eq!(post_states.len(), 1);
|
||||
assert_eq!(
|
||||
post_states[0].required_claim(),
|
||||
Some(Claim::Pda(compute_config_pda_seed()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stores_token_program_id() {
|
||||
let post_states = initialize(config_uninit(), TOKEN_PROGRAM_ID, AMM_PROGRAM_ID);
|
||||
let config = AmmConfig::try_from(&post_states[0].account().data)
|
||||
.expect("post state must contain a valid AmmConfig");
|
||||
assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "AMM config Account ID does not match PDA")]
|
||||
fn wrong_config_account_id_panics() {
|
||||
let mut wrong = config_uninit();
|
||||
wrong.account_id = AccountId::new([0; 32]);
|
||||
initialize(wrong, TOKEN_PROGRAM_ID, AMM_PROGRAM_ID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "AMM config account must be uninitialized")]
|
||||
fn already_initialized_config_panics() {
|
||||
let mut initialized = config_uninit();
|
||||
initialized.account.data = Data::from(&AmmConfig {
|
||||
token_program_id: TOKEN_PROGRAM_ID,
|
||||
});
|
||||
initialized.account.nonce = Nonce(0);
|
||||
initialize(initialized, TOKEN_PROGRAM_ID, AMM_PROGRAM_ID);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
pub use amm_core as core;
|
||||
|
||||
pub mod add;
|
||||
pub mod initialize;
|
||||
pub mod new_definition;
|
||||
pub mod remove;
|
||||
pub mod swap;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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,
|
||||
assert_supported_fee_tier, compute_config_pda, 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, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
@@ -17,6 +17,7 @@ use token_core::TokenDefinition;
|
||||
reason = "instruction surface passes explicit pool, vault, mint, lock, and user accounts"
|
||||
)]
|
||||
pub fn new_definition(
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
@@ -37,12 +38,24 @@ pub fn new_definition(
|
||||
.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
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// holding. Validating the config PDA is also the Program's initialization gate.
|
||||
assert_eq!(
|
||||
user_holding_b.account.program_owner, token_program,
|
||||
"User Token holdings must use the same Token Program"
|
||||
config.account_id,
|
||||
compute_config_pda(amm_program_id),
|
||||
"New definition: AMM config Account ID does not match PDA"
|
||||
);
|
||||
let token_program_id = AmmConfig::try_from(&config.account.data)
|
||||
.expect("New definition: AMM Program must be initialized before use")
|
||||
.token_program_id;
|
||||
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the configured Token Program"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_b.account.program_owner, token_program_id,
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
// Verify token_a and token_b are different
|
||||
assert!(
|
||||
@@ -124,8 +137,6 @@ pub fn new_definition(
|
||||
)),
|
||||
);
|
||||
|
||||
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;
|
||||
@@ -199,6 +210,7 @@ pub fn new_definition(
|
||||
];
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(config.account.clone()),
|
||||
pool_post.clone(),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::num::NonZeroU128;
|
||||
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, compute_liquidity_token_pda_seed, compute_vault_pda_seed,
|
||||
PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda_seed,
|
||||
compute_vault_pda_seed, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
|
||||
#[expect(
|
||||
@@ -14,6 +14,7 @@ use nssa_core::{
|
||||
reason = "instruction surface passes explicit pool, vault, and user accounts"
|
||||
)]
|
||||
pub fn remove_liquidity(
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
@@ -24,9 +25,21 @@ pub fn remove_liquidity(
|
||||
remove_liquidity_amount: NonZeroU128,
|
||||
min_amount_to_remove_token_a: u128,
|
||||
min_amount_to_remove_token_b: u128,
|
||||
amm_program_id: ProgramId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let remove_liquidity_amount: u128 = remove_liquidity_amount.into();
|
||||
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// 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"
|
||||
);
|
||||
let token_program_id = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Remove liquidity: AMM Program must be initialized before use")
|
||||
.token_program_id;
|
||||
|
||||
// 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");
|
||||
@@ -49,14 +62,21 @@ pub fn remove_liquidity(
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
let token_program_id = vault_a.account.program_owner;
|
||||
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"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the vault's Token Program"
|
||||
"User Token A holding must be owned by the configured 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"
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
|
||||
// Vault addresses do not need to be checked with PDA
|
||||
@@ -204,6 +224,7 @@ pub fn remove_liquidity(
|
||||
let chained_calls = vec![call_token_lp, call_token_b, call_token_a];
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(config.account.clone()),
|
||||
AccountPostState::new(pool_post.clone()),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use amm_core::{
|
||||
assert_supported_fee_tier, read_vault_fungible_balances, FEE_BPS_DENOMINATOR, MINIMUM_LIQUIDITY,
|
||||
assert_supported_fee_tier, compute_config_pda, read_vault_fungible_balances, AmmConfig,
|
||||
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},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
|
||||
/// Validates swap setup: checks pool liquidity is ready, vaults match, and reserves are sufficient.
|
||||
@@ -55,6 +56,7 @@ fn validate_swap_setup(
|
||||
reason = "consistent with codebase style"
|
||||
)]
|
||||
fn create_swap_post_states(
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
pool_def_data: PoolDefinition,
|
||||
vault_a: AccountWithMetadata,
|
||||
@@ -86,6 +88,7 @@ fn create_swap_post_states(
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
vec![
|
||||
AccountPostState::new(config.account),
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account),
|
||||
AccountPostState::new(vault_b.account),
|
||||
@@ -100,6 +103,7 @@ fn create_swap_post_states(
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn swap_exact_input(
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
@@ -108,17 +112,35 @@ pub fn swap_exact_input(
|
||||
swap_amount_in: u128,
|
||||
min_amount_out: u128,
|
||||
token_in_id: AccountId,
|
||||
amm_program_id: ProgramId,
|
||||
) -> (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;
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// account. Validating the config PDA is also the Program's initialization gate.
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
compute_config_pda(amm_program_id),
|
||||
"Swap exact input: AMM config Account ID does not match PDA"
|
||||
);
|
||||
let token_program_id = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Swap exact input: AMM Program must be initialized before use")
|
||||
.token_program_id;
|
||||
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"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the vault's Token Program"
|
||||
"User Token A holding must be owned by the configured 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"
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
|
||||
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||
@@ -157,6 +179,7 @@ pub fn swap_exact_input(
|
||||
};
|
||||
|
||||
let post_states = create_swap_post_states(
|
||||
config,
|
||||
pool,
|
||||
pool_def_data,
|
||||
vault_a,
|
||||
@@ -262,6 +285,7 @@ fn swap_logic(
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn swap_exact_output(
|
||||
config: AccountWithMetadata,
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
@@ -270,17 +294,35 @@ pub fn swap_exact_output(
|
||||
exact_amount_out: u128,
|
||||
max_amount_in: u128,
|
||||
token_in_id: AccountId,
|
||||
amm_program_id: ProgramId,
|
||||
) -> (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;
|
||||
// The Token Program is taken from the config account, not trusted from a caller-supplied
|
||||
// account. Validating the config PDA is also the Program's initialization gate.
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
compute_config_pda(amm_program_id),
|
||||
"Swap exact output: AMM config Account ID does not match PDA"
|
||||
);
|
||||
let token_program_id = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Swap exact output: AMM Program must be initialized before use")
|
||||
.token_program_id;
|
||||
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"
|
||||
);
|
||||
assert_eq!(
|
||||
user_holding_a.account.program_owner, token_program_id,
|
||||
"User Token A holding must be owned by the vault's Token Program"
|
||||
"User Token A holding must be owned by the configured 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"
|
||||
"User Token B holding must be owned by the configured Token Program"
|
||||
);
|
||||
|
||||
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||
@@ -319,6 +361,7 @@ pub fn swap_exact_output(
|
||||
};
|
||||
|
||||
let post_states = create_swap_post_states(
|
||||
config,
|
||||
pool,
|
||||
pool_def_data,
|
||||
vault_a,
|
||||
|
||||
+262
-29
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user