diff --git a/artifacts/amm-idl.json b/artifacts/amm-idl.json index d191404..6f12697 100644 --- a/artifacts/amm-idl.json +++ b/artifacts/amm-idl.json @@ -44,23 +44,9 @@ } ], "args": [ - { - "name": "token_program_id", - "type": { - "option": "program_id" - } - }, - { - "name": "twap_oracle_program_id", - "type": { - "option": "program_id" - } - }, { "name": "new_authority", - "type": { - "option": "account_id" - } + "type": "account_id" } ] }, diff --git a/programs/amm/core/src/lib.rs b/programs/amm/core/src/lib.rs index 4ca2765..8859a6a 100644 --- a/programs/amm/core/src/lib.rs +++ b/programs/amm/core/src/lib.rs @@ -21,7 +21,7 @@ pub enum Instruction { /// The configuration account is a PDA derived from the constant `"CONFIG"` seed /// (`compute_config_pda(self_program_id)`). It stores the program IDs the AMM issues chained /// calls to (the Token Program and the TWAP oracle program), plus the admin `authority` - /// allowed to change configuration later via `UpdateConfig`. The Program must be initialized + /// allowed to transfer admin control later via `UpdateConfig`. The Program must be initialized /// via this instruction before any pool can be created or interacted with — the other /// instructions read these program IDs from this account and reject calls when it does not /// yet exist. @@ -33,26 +33,26 @@ pub enum Instruction { token_program_id: ProgramId, /// Program ID of the TWAP oracle program the AMM will issue chained calls to. twap_oracle_program_id: ProgramId, - /// Admin authority allowed to change configuration via `UpdateConfig`. + /// Admin authority allowed to transfer admin control via `UpdateConfig`. authority: AccountId, }, - /// Updates the AMM Program's configuration. Only the configured admin `authority` may call - /// this; the authority account must be passed authorized (signed). + /// Transfers the AMM Program's admin authority to a new account. Only the configured admin + /// `authority` may call this; the authority account must be passed authorized (signed). /// - /// Each field is optional — `None` leaves the corresponding value unchanged. Setting - /// `new_authority` transfers admin control to a different account. + /// The Token Program and TWAP oracle program IDs are **immutable deployment parameters** set + /// once at `Initialize`: they are baked into every derived PDA (vaults, current-tick, + /// price-observation / price accounts) and into the AMM's chained calls, so changing them + /// after any pool exists would orphan every derived account and vault. They therefore cannot + /// be reconfigured in place — a genuine change requires redeploying the AMM. This instruction + /// only moves the admin authority. /// /// Required accounts: /// - AMM Config Account (initialized) /// - Authority Account — must equal the config's current `authority`, passed authorized. UpdateConfig { - /// New Token Program ID for chained calls, or `None` to keep the current one. - token_program_id: Option, - /// New TWAP oracle program ID for chained calls, or `None` to keep the current one. - twap_oracle_program_id: Option, - /// New admin authority (transfers control), or `None` to keep the current admin. - new_authority: Option, + /// New admin authority (transfers admin control to this account). + new_authority: AccountId, }, /// Creates a TWAP price-observations account for a pool over a time window, on behalf of the @@ -382,7 +382,7 @@ pub struct AmmConfig { pub token_program_id: ProgramId, /// Program ID of the TWAP oracle program the AMM issues chained calls to. pub twap_oracle_program_id: ProgramId, - /// Admin authority allowed to change this configuration via `UpdateConfig`. + /// Admin authority allowed to transfer admin control via `UpdateConfig`. pub authority: AccountId, } diff --git a/programs/amm/methods/guest/src/bin/amm.rs b/programs/amm/methods/guest/src/bin/amm.rs index a05db60..4f1a0fd 100644 --- a/programs/amm/methods/guest/src/bin/amm.rs +++ b/programs/amm/methods/guest/src/bin/amm.rs @@ -47,7 +47,9 @@ mod amm { Ok(spel_framework::SpelOutput::execute(post_states, vec![])) } - /// Updates the AMM Program's configuration. Only the configured admin authority may call this. + /// Transfers the AMM Program's admin authority. Only the configured admin authority may call + /// this. The Token Program and TWAP oracle program IDs are immutable deployment parameters and + /// cannot be changed here. /// /// Expected accounts: /// 1. `config` — initialized AMM config account. @@ -59,15 +61,11 @@ mod amm { config: AccountWithMetadata, #[account(signer)] authority: AccountWithMetadata, - token_program_id: Option, - twap_oracle_program_id: Option, - new_authority: Option, + new_authority: AccountId, ) -> SpelResult { let post_states = amm_program::update_config::update_config( config, authority, - token_program_id, - twap_oracle_program_id, new_authority, ctx.self_program_id, ); diff --git a/programs/amm/src/update_config.rs b/programs/amm/src/update_config.rs index e8742fd..2ff9fbf 100644 --- a/programs/amm/src/update_config.rs +++ b/programs/amm/src/update_config.rs @@ -4,14 +4,15 @@ use nssa_core::{ program::{AccountPostState, ProgramId}, }; -/// Updates the AMM Program's singleton configuration account. +/// Transfers the AMM Program's admin authority to a new account. /// /// Only the config's current admin `authority` may call this: the `authority` account must equal -/// the stored authority and be passed authorized (signed). Each field is optional — `None` leaves -/// the current value unchanged. Passing `new_authority` transfers admin control to a new account. +/// the stored authority and be passed authorized (signed). The new admin is `new_authority`. /// -/// The config account is already owned by this Program (created at `initialize`), so its data is -/// updated in place — no claim is required. +/// The Token Program and TWAP oracle program IDs are immutable deployment parameters (set once at +/// `initialize`) — baked into every derived PDA and the AMM's chained calls — so this instruction +/// cannot change them; it only moves the admin authority. The config account is already owned by +/// this Program (created at `initialize`), so its data is updated in place — no claim is required. /// /// # Panics /// Panics if: @@ -22,9 +23,7 @@ use nssa_core::{ pub fn update_config( config: AccountWithMetadata, authority: AccountWithMetadata, - token_program_id: Option, - twap_oracle_program_id: Option, - new_authority: Option, + new_authority: AccountId, amm_program_id: ProgramId, ) -> Vec { assert_eq!( @@ -45,15 +44,7 @@ pub fn update_config( "Update config: admin authority must authorize the update" ); - if let Some(token_program_id) = token_program_id { - config_data.token_program_id = token_program_id; - } - if let Some(twap_oracle_program_id) = twap_oracle_program_id { - config_data.twap_oracle_program_id = twap_oracle_program_id; - } - if let Some(new_authority) = new_authority { - config_data.authority = new_authority; - } + config_data.authority = new_authority; let mut config_post = config.account.clone(); config_post.data = Data::from(&config_data); @@ -72,14 +63,16 @@ mod tests { const AMM_PROGRAM_ID: ProgramId = [42; 8]; const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; - const NEW_TOKEN_PROGRAM_ID: ProgramId = [16; 8]; const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8]; - const NEW_TWAP_ORACLE_PROGRAM_ID: ProgramId = [78; 8]; fn admin_id() -> AccountId { AccountId::new([9; 32]) } + fn new_admin_id() -> AccountId { + AccountId::new([7; 32]) + } + fn config_init() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -112,87 +105,19 @@ mod tests { // ── happy path ──────────────────────────────────────────────────────────── - #[test] - fn updates_token_program_id() { - let post_states = update_config( - config_init(), - admin_authorized(), - Some(NEW_TOKEN_PROGRAM_ID), - None, - None, - AMM_PROGRAM_ID, - ); - let config = updated_config(&post_states); - assert_eq!(config.token_program_id, NEW_TOKEN_PROGRAM_ID); - // TWAP oracle program and authority are unchanged. - assert_eq!(config.twap_oracle_program_id, TWAP_ORACLE_PROGRAM_ID); - assert_eq!(config.authority, admin_id()); - } - - #[test] - fn updates_twap_oracle_program_id() { - let post_states = update_config( - config_init(), - admin_authorized(), - None, - Some(NEW_TWAP_ORACLE_PROGRAM_ID), - None, - AMM_PROGRAM_ID, - ); - let config = updated_config(&post_states); - assert_eq!(config.twap_oracle_program_id, NEW_TWAP_ORACLE_PROGRAM_ID); - // Token program and authority are unchanged. - assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID); - assert_eq!(config.authority, admin_id()); - } - #[test] fn transfers_authority() { - let new_admin = AccountId::new([7; 32]); let post_states = update_config( config_init(), admin_authorized(), - None, - None, - Some(new_admin), - AMM_PROGRAM_ID, - ); - let config = updated_config(&post_states); - assert_eq!(config.authority, new_admin); - // Token program is unchanged. - assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID); - } - - #[test] - fn updates_both_fields() { - let new_admin = AccountId::new([7; 32]); - let post_states = update_config( - config_init(), - admin_authorized(), - Some(NEW_TOKEN_PROGRAM_ID), - None, - Some(new_admin), - AMM_PROGRAM_ID, - ); - let config = updated_config(&post_states); - assert_eq!(config.token_program_id, NEW_TOKEN_PROGRAM_ID); - assert_eq!(config.authority, new_admin); - } - - #[test] - fn no_op_update_leaves_config_unchanged() { - let post_states = update_config( - config_init(), - admin_authorized(), - None, - None, - None, + new_admin_id(), AMM_PROGRAM_ID, ); let config = updated_config(&post_states); + assert_eq!(config.authority, new_admin_id()); + // The immutable program IDs are untouched — they cannot be changed here. assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID); assert_eq!(config.twap_oracle_program_id, TWAP_ORACLE_PROGRAM_ID); - assert_eq!(config.authority, admin_id()); } #[test] @@ -201,9 +126,7 @@ mod tests { let post_states = update_config( config_init(), authority.clone(), - Some(NEW_TOKEN_PROGRAM_ID), - None, - None, + new_admin_id(), AMM_PROGRAM_ID, ); assert_eq!(post_states.len(), 2); @@ -219,14 +142,7 @@ mod tests { fn wrong_config_pda_panics() { let mut config = config_init(); config.account_id = AccountId::new([0; 32]); - update_config( - config, - admin_authorized(), - Some(NEW_TOKEN_PROGRAM_ID), - None, - None, - AMM_PROGRAM_ID, - ); + update_config(config, admin_authorized(), new_admin_id(), AMM_PROGRAM_ID); } #[test] @@ -237,14 +153,7 @@ mod tests { is_authorized: false, account_id: compute_config_pda(AMM_PROGRAM_ID), }; - update_config( - config, - admin_authorized(), - Some(NEW_TOKEN_PROGRAM_ID), - None, - None, - AMM_PROGRAM_ID, - ); + update_config(config, admin_authorized(), new_admin_id(), AMM_PROGRAM_ID); } /// A caller who is not the configured admin cannot change the config, even if they sign. @@ -253,14 +162,7 @@ mod tests { fn non_admin_authority_panics() { let mut not_admin = admin_authorized(); not_admin.account_id = AccountId::new([123; 32]); - update_config( - config_init(), - not_admin, - Some(NEW_TOKEN_PROGRAM_ID), - None, - None, - AMM_PROGRAM_ID, - ); + update_config(config_init(), not_admin, new_admin_id(), AMM_PROGRAM_ID); } /// The admin account must actually sign; passing it unauthorized is rejected. @@ -269,13 +171,6 @@ mod tests { fn unauthorized_admin_panics() { let mut unsigned = admin_authorized(); unsigned.is_authorized = false; - update_config( - config_init(), - unsigned, - Some(NEW_TOKEN_PROGRAM_ID), - None, - None, - AMM_PROGRAM_ID, - ); + update_config(config_init(), unsigned, new_admin_id(), AMM_PROGRAM_ID); } } diff --git a/programs/integration_tests/tests/amm.rs b/programs/integration_tests/tests/amm.rs index 6d6896a..539c9b9 100644 --- a/programs/integration_tests/tests/amm.rs +++ b/programs/integration_tests/tests/amm.rs @@ -1639,16 +1639,10 @@ fn amm_initialize_creates_config_account() { fn execute_update_config( state: &mut V03State, signer: &PrivateKey, - token_program_id: Option, - twap_oracle_program_id: Option, - new_authority: Option, + new_authority: AccountId, ) -> Result<(), LeeError> { let signer_id = AccountId::from(&PublicKey::new_from_private_key(signer)); - let instruction = amm_core::Instruction::UpdateConfig { - token_program_id, - twap_oracle_program_id, - new_authority, - }; + let instruction = amm_core::Instruction::UpdateConfig { new_authority }; let message = public_transaction::Message::try_new( Ids::amm_program(), @@ -1676,38 +1670,31 @@ fn initialized_amm_state() -> V03State { } #[test] -fn amm_update_config_changes_token_program_id_and_authority() { +fn amm_update_config_transfers_authority_and_keeps_program_ids() { let mut state = initialized_amm_state(); - - let new_token_program = [123u32; 8]; let new_admin = Ids::user_a(); - execute_update_config( - &mut state, - &Keys::admin(), - Some(new_token_program), - None, - Some(new_admin), - ) - .unwrap(); + let before = config_data(&state); + execute_update_config(&mut state, &Keys::admin(), new_admin).unwrap(); + let after = config_data(&state); - let config = config_data(&state); - assert_eq!(config.token_program_id, new_token_program); - assert_eq!(config.authority, new_admin); + assert_eq!(after.authority, new_admin); + // The Token and TWAP oracle program IDs are immutable deployment parameters — the instruction + // has no field to change them, so they are unaffected by an authority transfer. + assert_eq!(after.token_program_id, before.token_program_id); + assert_eq!(after.twap_oracle_program_id, before.twap_oracle_program_id); } #[test] fn amm_update_config_rejects_non_admin() { let mut state = initialized_amm_state(); - // user_a is not the admin; even though they sign, the update is rejected and the config is + // user_a is not the admin; even though they sign, the transfer is rejected and the config is // left unchanged. - let result = execute_update_config(&mut state, &Keys::user_a(), Some([123u32; 8]), None, None); + let result = execute_update_config(&mut state, &Keys::user_a(), Ids::user_a()); assert!(matches!(result, Err(LeeError::ProgramExecutionFailed(_)))); - let config = config_data(&state); - assert_eq!(config.token_program_id, Ids::token_program()); - assert_eq!(config.authority, Ids::admin()); + assert_eq!(config_data(&state).authority, Ids::admin()); } #[test] @@ -1716,16 +1703,17 @@ fn amm_update_config_authority_handoff_revokes_old_admin() { let new_admin = Ids::user_a(); // Admin hands off control to user_a. - execute_update_config(&mut state, &Keys::admin(), None, None, Some(new_admin)).unwrap(); + execute_update_config(&mut state, &Keys::admin(), new_admin).unwrap(); assert_eq!(config_data(&state).authority, new_admin); - // The original admin can no longer update. - let result = execute_update_config(&mut state, &Keys::admin(), Some([123u32; 8]), None, None); + // The original admin can no longer transfer authority. + let result = execute_update_config(&mut state, &Keys::admin(), Ids::admin()); assert!(matches!(result, Err(LeeError::ProgramExecutionFailed(_)))); + assert_eq!(config_data(&state).authority, new_admin); // The new admin can. - execute_update_config(&mut state, &Keys::user_a(), Some([124u32; 8]), None, None).unwrap(); - assert_eq!(config_data(&state).token_program_id, [124u32; 8]); + execute_update_config(&mut state, &Keys::user_a(), Ids::admin()).unwrap(); + assert_eq!(config_data(&state).authority, Ids::admin()); } #[test]