mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
fix(amm)!: restrict UpdateConfig to authority transfer only
UpdateConfig let the admin rewrite token_program_id and twap_oracle_program_id in place. Both are immutable deployment parameters — twap_oracle_program_id derives the current-tick PDA and every price-observation/price account PDA, and token_program_id is the program the AMM issues its vault transfers to — so changing either after any pool exists would orphan every derived account and vault. A genuine change requires redeploying the AMM, never an in-place edit. - Instruction::UpdateConfig now carries a single required field, new_authority; the two program-id fields are removed. - update_config assigns the new authority directly (no Option/if-let); PDA + admin + signature preconditions unchanged. - Guest handler and IDL updated to match. - Drop the integration test that mutated token_program_id (it exercised the vulnerability); keep reject-non-admin and authority-handoff, and assert program ids survive a transfer. BREAKING CHANGE: the UpdateConfig instruction ABI changed — the token_program_id and twap_oracle_program_id fields are removed and new_authority is now required (was Option). Any client constructing UpdateConfig must be updated. The instruction enum change also alters the program ImageID: redeploy and update every ImageID-derived value (deployed program ids, client/config files, PDA-derived addresses, AMM/ATA program-id inputs) before submitting
This commit is contained in:
@@ -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<ProgramId>,
|
||||
/// New TWAP oracle program ID for chained calls, or `None` to keep the current one.
|
||||
twap_oracle_program_id: Option<ProgramId>,
|
||||
/// New admin authority (transfers control), or `None` to keep the current admin.
|
||||
new_authority: Option<AccountId>,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ProgramId>,
|
||||
twap_oracle_program_id: Option<ProgramId>,
|
||||
new_authority: Option<AccountId>,
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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<ProgramId>,
|
||||
twap_oracle_program_id: Option<ProgramId>,
|
||||
new_authority: Option<AccountId>,
|
||||
new_authority: AccountId,
|
||||
amm_program_id: ProgramId,
|
||||
) -> Vec<AccountPostState> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1639,16 +1639,10 @@ fn amm_initialize_creates_config_account() {
|
||||
fn execute_update_config(
|
||||
state: &mut V03State,
|
||||
signer: &PrivateKey,
|
||||
token_program_id: Option<nssa_core::program::ProgramId>,
|
||||
twap_oracle_program_id: Option<nssa_core::program::ProgramId>,
|
||||
new_authority: Option<AccountId>,
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user