2026-03-17 18:08:53 +01:00
|
|
|
use std::num::NonZeroU128;
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
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,
|
fix(amm): compute pool arithmetic in u256 to avoid u128 overflow
The AMM multiplied amounts in u128 — `token_a * token_b` for the initial
LP in `new_definition`, `reserve * amount` in swaps, and the mul/div steps
in add/remove liquidity. For realistic 18-decimal token amounts the
intermediate product exceeds `u128::MAX` (~3.4e38): opening a pool with
100/200 tokens is `1e20 * 2e20 = 2e40`, which panicked and caused the
sequencer to skip the transaction.
Widen the intermediate arithmetic, not the stored types. Add
`mul_div_floor`, `mul_div_ceil`, and `isqrt_product` to `amm_core` (using
`alloy_primitives::U256`, as `spot_price_q64_64` already does): they
compute the product/division/sqrt in U256 and downcast the result back to
u128. Route `new_definition`, `swap_exact_input`/`swap_exact_output`,
`add_liquidity`, and `remove_liquidity` through them. `swap_exact_output`
keeps its ceil rounding (required input rounded up, in the pool's favour)
via `mul_div_ceil`.
Balances, reserves, and LP supply stay u128, so account data formats,
IDLs, and the token/ata/stablecoin programs are unchanged. This lifts the
usable amount range to the full u128.
2026-06-29 08:56:48 +02:00
|
|
|
compute_vault_pda_seed, isqrt_product, spot_price_q64_64, AmmConfig, PoolDefinition,
|
|
|
|
|
MINIMUM_LIQUIDITY,
|
2026-03-17 18:08:53 +01:00
|
|
|
};
|
2026-06-18 14:07:04 +02:00
|
|
|
use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
2026-03-17 18:08:53 +01:00
|
|
|
use nssa_core::{
|
|
|
|
|
account::{Account, AccountWithMetadata, Data},
|
2026-04-15 14:55:04 -03:00
|
|
|
program::{AccountPostState, ChainedCall, Claim, ProgramId},
|
2026-03-17 18:08:53 +01:00
|
|
|
};
|
2026-04-08 17:48:13 -03:00
|
|
|
use token_core::TokenDefinition;
|
2026-06-18 14:07:04 +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, mint, lock, and user accounts"
|
|
|
|
|
)]
|
2026-03-17 18:08:53 +01:00
|
|
|
pub fn new_definition(
|
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,
|
2026-04-08 17:48:13 -03:00
|
|
|
lp_lock_holding: AccountWithMetadata,
|
2026-03-17 18:08:53 +01:00
|
|
|
user_holding_a: AccountWithMetadata,
|
|
|
|
|
user_holding_b: AccountWithMetadata,
|
|
|
|
|
user_holding_lp: AccountWithMetadata,
|
2026-06-18 14:07:04 +02:00
|
|
|
current_tick_account: AccountWithMetadata,
|
|
|
|
|
clock: AccountWithMetadata,
|
2026-03-17 18:08:53 +01:00
|
|
|
token_a_amount: NonZeroU128,
|
|
|
|
|
token_b_amount: NonZeroU128,
|
2026-03-31 20:45:57 -03:00
|
|
|
fees: u128,
|
2026-03-17 18:08:53 +01:00
|
|
|
amm_program_id: ProgramId,
|
|
|
|
|
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
|
|
|
|
let definition_token_a_id = token_core::TokenHolding::try_from(&user_holding_a.account.data)
|
|
|
|
|
.expect("New definition: AMM Program expects valid Token Holding account for Token A")
|
|
|
|
|
.definition_id();
|
|
|
|
|
let definition_token_b_id = token_core::TokenHolding::try_from(&user_holding_b.account.data)
|
|
|
|
|
.expect("New definition: AMM Program expects valid Token Holding account for Token B")
|
|
|
|
|
.definition_id();
|
|
|
|
|
|
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
|
|
|
// 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),
|
|
|
|
|
"New definition: AMM config Account ID does not match PDA"
|
|
|
|
|
);
|
2026-06-18 14:07:04 +02:00
|
|
|
let config_data = AmmConfig::try_from(&config.account.data)
|
|
|
|
|
.expect("New definition: 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;
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
assert_eq!(
|
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_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"
|
2026-03-17 18:08:53 +01:00
|
|
|
);
|
2026-04-13 17:11:28 +02:00
|
|
|
// Verify token_a and token_b are different
|
2026-03-17 18:08:53 +01:00
|
|
|
assert!(
|
|
|
|
|
definition_token_a_id != definition_token_b_id,
|
|
|
|
|
"Cannot set up a swap for a token with itself"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pool.account_id,
|
|
|
|
|
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id),
|
|
|
|
|
"Pool Definition Account ID does not match PDA"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
vault_a.account_id,
|
|
|
|
|
compute_vault_pda(amm_program_id, pool.account_id, definition_token_a_id),
|
|
|
|
|
"Vault ID does not match PDA"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
vault_b.account_id,
|
|
|
|
|
compute_vault_pda(amm_program_id, pool.account_id, definition_token_b_id),
|
|
|
|
|
"Vault ID does not match PDA"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pool_definition_lp.account_id,
|
|
|
|
|
compute_liquidity_token_pda(amm_program_id, pool.account_id),
|
|
|
|
|
"Liquidity pool Token Definition Account ID does not match PDA"
|
|
|
|
|
);
|
2026-04-08 17:48:13 -03:00
|
|
|
assert_eq!(
|
|
|
|
|
lp_lock_holding.account_id,
|
|
|
|
|
compute_lp_lock_holding_pda(amm_program_id, pool.account_id),
|
|
|
|
|
"LP lock holding Account ID does not match PDA"
|
|
|
|
|
);
|
2026-03-31 20:45:57 -03:00
|
|
|
assert_supported_fee_tier(fees);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-04-13 17:11:28 +02:00
|
|
|
// Assert that pool is uninitialized (hard precondition)
|
2026-04-10 15:43:13 -03:00
|
|
|
assert_eq!(
|
2026-04-13 17:11:28 +02:00
|
|
|
pool.account,
|
|
|
|
|
Account::default(),
|
|
|
|
|
"Pool account must be uninitialized"
|
2026-03-17 18:08:53 +01:00
|
|
|
);
|
2026-04-15 14:55:04 -03:00
|
|
|
assert!(
|
|
|
|
|
user_holding_lp.account != Account::default() || user_holding_lp.is_authorized,
|
|
|
|
|
"Fresh user LP holding requires user authorization"
|
|
|
|
|
);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-06-18 14:07:04 +02:00
|
|
|
// The pool's TWAP current-tick account is created in the same transaction (a chained call to
|
|
|
|
|
// the oracle). Validate its PDA and that the clock is the canonical 1-block LEZ clock.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
current_tick_account.account_id,
|
|
|
|
|
compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id),
|
|
|
|
|
"New definition: current tick Account ID does not match PDA"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
|
|
|
|
"New definition: clock account must be the canonical 1-block LEZ clock account"
|
|
|
|
|
);
|
|
|
|
|
|
fix(amm): compute pool arithmetic in u256 to avoid u128 overflow
The AMM multiplied amounts in u128 — `token_a * token_b` for the initial
LP in `new_definition`, `reserve * amount` in swaps, and the mul/div steps
in add/remove liquidity. For realistic 18-decimal token amounts the
intermediate product exceeds `u128::MAX` (~3.4e38): opening a pool with
100/200 tokens is `1e20 * 2e20 = 2e40`, which panicked and caused the
sequencer to skip the transaction.
Widen the intermediate arithmetic, not the stored types. Add
`mul_div_floor`, `mul_div_ceil`, and `isqrt_product` to `amm_core` (using
`alloy_primitives::U256`, as `spot_price_q64_64` already does): they
compute the product/division/sqrt in U256 and downcast the result back to
u128. Route `new_definition`, `swap_exact_input`/`swap_exact_output`,
`add_liquidity`, and `remove_liquidity` through them. `swap_exact_output`
keeps its ceil rounding (required input rounded up, in the pool's favour)
via `mul_div_ceil`.
Balances, reserves, and LP supply stay u128, so account data formats,
IDLs, and the token/ata/stablecoin programs are unchanged. This lifts the
usable amount range to the full u128.
2026-06-29 08:56:48 +02:00
|
|
|
// LP Token minting calculation. The `token_a * token_b` product is computed in U256 (via
|
|
|
|
|
// `isqrt_product`) so realistic 18-decimal amounts can't overflow u128 before the sqrt.
|
|
|
|
|
let initial_lp = isqrt_product(token_a_amount.get(), token_b_amount.get());
|
2026-04-08 17:48:13 -03:00
|
|
|
assert!(
|
|
|
|
|
initial_lp > MINIMUM_LIQUIDITY,
|
|
|
|
|
"Initial liquidity must exceed minimum liquidity lock"
|
|
|
|
|
);
|
2026-05-06 17:08:15 -03:00
|
|
|
let user_lp = initial_lp
|
|
|
|
|
.checked_sub(MINIMUM_LIQUIDITY)
|
|
|
|
|
.expect("initial liquidity must exceed minimum liquidity after validation");
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
// Update pool account
|
|
|
|
|
let pool_post_definition = PoolDefinition {
|
|
|
|
|
definition_token_a_id,
|
|
|
|
|
definition_token_b_id,
|
|
|
|
|
vault_a_id: vault_a.account_id,
|
|
|
|
|
vault_b_id: vault_b.account_id,
|
|
|
|
|
liquidity_pool_id: pool_definition_lp.account_id,
|
|
|
|
|
liquidity_pool_supply: initial_lp,
|
|
|
|
|
reserve_a: token_a_amount.into(),
|
|
|
|
|
reserve_b: token_b_amount.into(),
|
2026-03-31 20:45:57 -03:00
|
|
|
fees,
|
2026-03-17 18:08:53 +01:00
|
|
|
};
|
|
|
|
|
|
2026-06-18 14:07:04 +02:00
|
|
|
let mut pool_initialized = pool.account.clone();
|
|
|
|
|
pool_initialized.data = Data::from(&pool_post_definition);
|
2026-04-15 14:55:04 -03:00
|
|
|
let pool_post: AccountPostState = AccountPostState::new_claimed(
|
2026-06-18 14:07:04 +02:00
|
|
|
pool_initialized.clone(),
|
2026-04-15 14:55:04 -03:00
|
|
|
Claim::Pda(compute_pool_pda_seed(
|
|
|
|
|
definition_token_a_id,
|
|
|
|
|
definition_token_b_id,
|
|
|
|
|
)),
|
|
|
|
|
);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
// Chain call for Token A (user_holding_a -> Vault_A)
|
2026-04-15 14:55:04 -03:00
|
|
|
let mut vault_a_authorized = vault_a.clone();
|
|
|
|
|
vault_a_authorized.is_authorized = true;
|
2026-03-17 18:08:53 +01:00
|
|
|
let call_token_a = ChainedCall::new(
|
|
|
|
|
token_program_id,
|
2026-04-15 14:55:04 -03:00
|
|
|
vec![user_holding_a.clone(), vault_a_authorized],
|
2026-03-17 18:08:53 +01:00
|
|
|
&token_core::Instruction::Transfer {
|
|
|
|
|
amount_to_transfer: token_a_amount.into(),
|
|
|
|
|
},
|
2026-04-15 14:55:04 -03:00
|
|
|
)
|
|
|
|
|
.with_pda_seeds(vec![compute_vault_pda_seed(
|
|
|
|
|
pool.account_id,
|
|
|
|
|
definition_token_a_id,
|
|
|
|
|
)]);
|
2026-03-17 18:08:53 +01:00
|
|
|
// Chain call for Token B (user_holding_b -> Vault_B)
|
2026-04-15 14:55:04 -03:00
|
|
|
let mut vault_b_authorized = vault_b.clone();
|
|
|
|
|
vault_b_authorized.is_authorized = true;
|
2026-03-17 18:08:53 +01:00
|
|
|
let call_token_b = ChainedCall::new(
|
|
|
|
|
token_program_id,
|
2026-04-15 14:55:04 -03:00
|
|
|
vec![user_holding_b.clone(), vault_b_authorized],
|
2026-03-17 18:08:53 +01:00
|
|
|
&token_core::Instruction::Transfer {
|
|
|
|
|
amount_to_transfer: token_b_amount.into(),
|
|
|
|
|
},
|
2026-04-15 14:55:04 -03:00
|
|
|
)
|
|
|
|
|
.with_pda_seeds(vec![compute_vault_pda_seed(
|
|
|
|
|
pool.account_id,
|
|
|
|
|
definition_token_b_id,
|
|
|
|
|
)]);
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
// Chain call for liquidity token lock holding
|
2026-03-17 18:08:53 +01:00
|
|
|
let mut pool_lp_auth = pool_definition_lp.clone();
|
|
|
|
|
pool_lp_auth.is_authorized = true;
|
2026-04-15 14:55:04 -03:00
|
|
|
let mut lp_lock_holding_auth = lp_lock_holding.clone();
|
|
|
|
|
lp_lock_holding_auth.is_authorized = true;
|
2026-03-17 18:08:53 +01:00
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
let call_token_lp_lock = ChainedCall::new(
|
|
|
|
|
token_program_id,
|
2026-04-15 14:55:04 -03:00
|
|
|
vec![pool_lp_auth.clone(), lp_lock_holding_auth],
|
2026-04-13 17:11:28 +02:00
|
|
|
&token_core::Instruction::NewFungibleDefinition {
|
|
|
|
|
name: String::from("LP Token"),
|
|
|
|
|
total_supply: MINIMUM_LIQUIDITY,
|
feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-05-27 15:04:28 +05:30
|
|
|
mint_authority: Some(pool_definition_lp.account_id),
|
2026-04-13 17:11:28 +02:00
|
|
|
},
|
2026-04-08 17:48:13 -03:00
|
|
|
)
|
2026-04-15 14:55:04 -03:00
|
|
|
.with_pda_seeds(vec![
|
|
|
|
|
compute_liquidity_token_pda_seed(pool.account_id),
|
|
|
|
|
compute_lp_lock_holding_pda_seed(pool.account_id),
|
|
|
|
|
]);
|
2026-04-08 17:48:13 -03:00
|
|
|
|
|
|
|
|
let mut pool_lp_after_lock = pool_lp_auth.clone();
|
2026-04-13 17:11:28 +02:00
|
|
|
pool_lp_after_lock.account.program_owner = token_program_id;
|
|
|
|
|
pool_lp_after_lock.account.data = Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
name: String::from("LP Token"),
|
|
|
|
|
total_supply: MINIMUM_LIQUIDITY,
|
|
|
|
|
metadata_id: None,
|
feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-05-27 15:04:28 +05:30
|
|
|
// Self-authority: the LP token is mintable only by the pool, which
|
|
|
|
|
// presents this PDA as the authorized minter in the chained Mint call.
|
|
|
|
|
authority: Some(pool_definition_lp.account_id),
|
2026-04-13 17:11:28 +02:00
|
|
|
});
|
2026-04-08 17:48:13 -03:00
|
|
|
let call_token_lp_user = ChainedCall::new(
|
2026-03-17 18:08:53 +01:00
|
|
|
token_program_id,
|
2026-04-08 17:48:13 -03:00
|
|
|
vec![pool_lp_after_lock, user_holding_lp.clone()],
|
|
|
|
|
&token_core::Instruction::Mint {
|
|
|
|
|
amount_to_mint: user_lp,
|
|
|
|
|
},
|
2026-03-17 18:08:53 +01:00
|
|
|
)
|
|
|
|
|
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
|
|
|
|
|
|
2026-06-18 14:07:04 +02:00
|
|
|
// Chain call to create the pool's TWAP current-tick account, with the pool as the price
|
|
|
|
|
// source. The oracle derives the tick from the opening spot price (reserve_b / reserve_a as a
|
|
|
|
|
// Q64.64 ratio), so the seed value is taken from the pool's own reserves, not the caller.
|
|
|
|
|
//
|
|
|
|
|
// The pool is claimed (and thus owned by this program) by this same instruction, so the
|
|
|
|
|
// chained call must present the pool in its post-claim state to match the accumulated state
|
|
|
|
|
// diff: the runtime sets the claimed pool's owner to this program, so we predict that here.
|
|
|
|
|
let initial_price = spot_price_q64_64(token_a_amount.get(), token_b_amount.get());
|
|
|
|
|
let mut pool_price_source_account = pool_initialized;
|
|
|
|
|
pool_price_source_account.program_owner = amm_program_id;
|
|
|
|
|
let pool_price_source = AccountWithMetadata {
|
|
|
|
|
account: pool_price_source_account,
|
|
|
|
|
is_authorized: true,
|
|
|
|
|
account_id: pool.account_id,
|
|
|
|
|
};
|
|
|
|
|
let call_create_current_tick = ChainedCall::new(
|
|
|
|
|
twap_oracle_program_id,
|
|
|
|
|
vec![
|
|
|
|
|
current_tick_account.clone(),
|
|
|
|
|
pool_price_source,
|
|
|
|
|
clock.clone(),
|
|
|
|
|
],
|
|
|
|
|
&twap_oracle_core::Instruction::CreateCurrentTickAccount { initial_price },
|
|
|
|
|
)
|
|
|
|
|
.with_pda_seeds(vec![compute_pool_pda_seed(
|
|
|
|
|
definition_token_a_id,
|
|
|
|
|
definition_token_b_id,
|
|
|
|
|
)]);
|
|
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
let chained_calls = vec![
|
|
|
|
|
call_token_lp_lock,
|
|
|
|
|
call_token_lp_user,
|
|
|
|
|
call_token_b,
|
|
|
|
|
call_token_a,
|
2026-06-18 14:07:04 +02:00
|
|
|
call_create_current_tick,
|
2026-04-08 17:48:13 -03:00
|
|
|
];
|
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
|
|
|
pool_post.clone(),
|
|
|
|
|
AccountPostState::new(vault_a.account.clone()),
|
|
|
|
|
AccountPostState::new(vault_b.account.clone()),
|
|
|
|
|
AccountPostState::new(pool_definition_lp.account.clone()),
|
2026-04-08 17:48:13 -03:00
|
|
|
AccountPostState::new(lp_lock_holding.account.clone()),
|
2026-03-17 18:08:53 +01:00
|
|
|
AccountPostState::new(user_holding_a.account.clone()),
|
|
|
|
|
AccountPostState::new(user_holding_b.account.clone()),
|
|
|
|
|
AccountPostState::new(user_holding_lp.account.clone()),
|
2026-06-18 14:07:04 +02:00
|
|
|
AccountPostState::new(current_tick_account.account.clone()),
|
|
|
|
|
AccountPostState::new(clock.account.clone()),
|
2026-03-17 18:08:53 +01:00
|
|
|
];
|
|
|
|
|
|
2026-04-08 17:48:13 -03:00
|
|
|
(post_states, chained_calls)
|
2026-03-17 18:08:53 +01:00
|
|
|
}
|