mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-06-28 11:10:08 +00:00
feat(amm): add admin authority and UpdateConfig instruction
Add an admin authority to the AMM config so configuration can be changed after initialization. AmmConfig gains an `authority` field, set by Initialize, and a new UpdateConfig instruction lets that admin change config values. UpdateConfig is access-controlled: the authority account must equal the stored config.authority and be passed authorized (signed). Both fields are optional — token_program_id updates the chained-call token program, and new_authority transfers admin control to a different account. Without this gate any caller could repoint the AMM at a malicious token program.
This commit is contained in:
parent
222c01e7d6
commit
1d9e3dcb49
@ -16,6 +16,41 @@
|
||||
{
|
||||
"name": "token_program_id",
|
||||
"type": "program_id"
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": "account_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "update_config",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "config",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "token_program_id",
|
||||
"type": {
|
||||
"option": "program_id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "new_authority",
|
||||
"type": {
|
||||
"option": "account_id"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -434,6 +469,10 @@
|
||||
{
|
||||
"name": "token_program_id",
|
||||
"type": "program_id"
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": "account_id"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -20,7 +20,8 @@ pub enum Instruction {
|
||||
///
|
||||
/// The configuration account is a PDA derived from the constant `"CONFIG"` seed
|
||||
/// (`compute_config_pda(self_program_id)`). It stores the Token Program ID that the AMM
|
||||
/// uses for every chained call. The Program must be initialized via this instruction before
|
||||
/// uses for every chained call, plus the admin `authority` allowed to change configuration
|
||||
/// later via `UpdateConfig`. The Program must be initialized via this instruction before
|
||||
/// any pool can be created or interacted with — the other instructions read the Token
|
||||
/// Program ID from this account and reject calls when it does not yet exist.
|
||||
///
|
||||
@ -29,6 +30,24 @@ pub enum Instruction {
|
||||
Initialize {
|
||||
/// Program ID of the Token Program the AMM will issue chained calls to.
|
||||
token_program_id: ProgramId,
|
||||
/// Admin authority allowed to change configuration 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).
|
||||
///
|
||||
/// Each field is optional — `None` leaves the corresponding value unchanged. Setting
|
||||
/// `new_authority` transfers admin control to a different account.
|
||||
///
|
||||
/// 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 admin authority (transfers control), or `None` to keep the current admin.
|
||||
new_authority: Option<AccountId>,
|
||||
},
|
||||
|
||||
/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
|
||||
@ -204,6 +223,8 @@ impl From<&PoolDefinition> for Data {
|
||||
pub struct AmmConfig {
|
||||
/// Program ID of the Token Program the AMM issues chained calls to.
|
||||
pub token_program_id: ProgramId,
|
||||
/// Admin authority allowed to change this configuration via `UpdateConfig`.
|
||||
pub authority: AccountId,
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for AmmConfig {
|
||||
|
||||
@ -29,9 +29,37 @@ mod amm {
|
||||
ctx: ProgramContext,
|
||||
config: AccountWithMetadata,
|
||||
token_program_id: ProgramId,
|
||||
authority: AccountId,
|
||||
) -> SpelResult {
|
||||
let post_states =
|
||||
amm_program::initialize::initialize(config, token_program_id, ctx.self_program_id);
|
||||
let post_states = amm_program::initialize::initialize(
|
||||
config,
|
||||
token_program_id,
|
||||
authority,
|
||||
ctx.self_program_id,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
|
||||
}
|
||||
|
||||
/// Updates the AMM Program's configuration. Only the configured admin authority may call this.
|
||||
///
|
||||
/// Expected accounts:
|
||||
/// 1. `config` — initialized AMM config account.
|
||||
/// 2. `authority` — the config's current admin, passed authorized (signed).
|
||||
#[instruction]
|
||||
pub fn update_config(
|
||||
ctx: ProgramContext,
|
||||
config: AccountWithMetadata,
|
||||
authority: AccountWithMetadata,
|
||||
token_program_id: Option<ProgramId>,
|
||||
new_authority: Option<AccountId>,
|
||||
) -> SpelResult {
|
||||
let post_states = amm_program::update_config::update_config(
|
||||
config,
|
||||
authority,
|
||||
token_program_id,
|
||||
new_authority,
|
||||
ctx.self_program_id,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
use amm_core::{compute_config_pda, compute_config_pda_seed, AmmConfig};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
account::{Account, AccountId, 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
|
||||
/// (`compute_config_pda(amm_program_id)`) and stores `token_program_id` (the Token Program the
|
||||
/// AMM issues every chained call to) and `authority` (the admin allowed to change configuration
|
||||
/// later via `update_config`). 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
|
||||
@ -18,6 +19,7 @@ use nssa_core::{
|
||||
pub fn initialize(
|
||||
config: AccountWithMetadata,
|
||||
token_program_id: ProgramId,
|
||||
authority: AccountId,
|
||||
amm_program_id: ProgramId,
|
||||
) -> Vec<AccountPostState> {
|
||||
assert_eq!(
|
||||
@ -32,7 +34,10 @@ pub fn initialize(
|
||||
);
|
||||
|
||||
let mut config_post = config.account.clone();
|
||||
config_post.data = Data::from(&AmmConfig { token_program_id });
|
||||
config_post.data = Data::from(&AmmConfig {
|
||||
token_program_id,
|
||||
authority,
|
||||
});
|
||||
|
||||
vec![AccountPostState::new_claimed(
|
||||
config_post,
|
||||
@ -50,6 +55,10 @@ mod tests {
|
||||
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
||||
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||
|
||||
fn authority() -> AccountId {
|
||||
AccountId::new([9; 32])
|
||||
}
|
||||
|
||||
fn config_uninit() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
@ -60,7 +69,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn returns_single_pda_claimed_post_state() {
|
||||
let post_states = initialize(config_uninit(), TOKEN_PROGRAM_ID, AMM_PROGRAM_ID);
|
||||
let post_states = initialize(
|
||||
config_uninit(),
|
||||
TOKEN_PROGRAM_ID,
|
||||
authority(),
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
assert_eq!(post_states.len(), 1);
|
||||
assert_eq!(
|
||||
post_states[0].required_claim(),
|
||||
@ -69,11 +83,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stores_token_program_id() {
|
||||
let post_states = initialize(config_uninit(), TOKEN_PROGRAM_ID, AMM_PROGRAM_ID);
|
||||
fn stores_token_program_id_and_authority() {
|
||||
let post_states = initialize(
|
||||
config_uninit(),
|
||||
TOKEN_PROGRAM_ID,
|
||||
authority(),
|
||||
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);
|
||||
assert_eq!(config.authority, authority());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -81,7 +101,7 @@ mod tests {
|
||||
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);
|
||||
initialize(wrong, TOKEN_PROGRAM_ID, authority(), AMM_PROGRAM_ID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -90,8 +110,9 @@ mod tests {
|
||||
let mut initialized = config_uninit();
|
||||
initialized.account.data = Data::from(&AmmConfig {
|
||||
token_program_id: TOKEN_PROGRAM_ID,
|
||||
authority: authority(),
|
||||
});
|
||||
initialized.account.nonce = Nonce(0);
|
||||
initialize(initialized, TOKEN_PROGRAM_ID, AMM_PROGRAM_ID);
|
||||
initialize(initialized, TOKEN_PROGRAM_ID, authority(), AMM_PROGRAM_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,5 +8,6 @@ pub mod new_definition;
|
||||
pub mod remove;
|
||||
pub mod swap;
|
||||
pub mod sync;
|
||||
pub mod update_config;
|
||||
|
||||
mod tests;
|
||||
|
||||
@ -628,6 +628,7 @@ impl AccountWithMetadataForTests {
|
||||
balance: 0u128,
|
||||
data: Data::from(&AmmConfig {
|
||||
token_program_id: TOKEN_PROGRAM_ID,
|
||||
authority: AccountId::new([9; 32]),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
|
||||
246
programs/amm/src/update_config.rs
Normal file
246
programs/amm/src/update_config.rs
Normal file
@ -0,0 +1,246 @@
|
||||
use amm_core::{compute_config_pda, AmmConfig};
|
||||
use nssa_core::{
|
||||
account::{AccountId, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ProgramId},
|
||||
};
|
||||
|
||||
/// Updates the AMM Program's singleton configuration 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 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:
|
||||
/// - `config.account_id` does not match `compute_config_pda(amm_program_id)`, or the config is
|
||||
/// uninitialized (the Program has not been initialized).
|
||||
/// - `authority.account_id` is not the config's current admin authority.
|
||||
/// - `authority.is_authorized` is false (the admin did not sign).
|
||||
pub fn update_config(
|
||||
config: AccountWithMetadata,
|
||||
authority: AccountWithMetadata,
|
||||
token_program_id: Option<ProgramId>,
|
||||
new_authority: Option<AccountId>,
|
||||
amm_program_id: ProgramId,
|
||||
) -> Vec<AccountPostState> {
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
compute_config_pda(amm_program_id),
|
||||
"Update config: AMM config Account ID does not match PDA"
|
||||
);
|
||||
let mut config_data = AmmConfig::try_from(&config.account.data)
|
||||
.expect("Update config: AMM Program must be initialized before use");
|
||||
|
||||
// Access control: the caller must be the configured admin and must have signed.
|
||||
assert_eq!(
|
||||
authority.account_id, config_data.authority,
|
||||
"Update config: caller is not the configured admin authority"
|
||||
);
|
||||
assert!(
|
||||
authority.is_authorized,
|
||||
"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(new_authority) = new_authority {
|
||||
config_data.authority = new_authority;
|
||||
}
|
||||
|
||||
let mut config_post = config.account.clone();
|
||||
config_post.data = Data::from(&config_data);
|
||||
|
||||
vec![
|
||||
AccountPostState::new(config_post),
|
||||
AccountPostState::new(authority.account.clone()),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nssa_core::account::{Account, Nonce};
|
||||
|
||||
use super::*;
|
||||
|
||||
const AMM_PROGRAM_ID: ProgramId = [42; 8];
|
||||
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||
const NEW_TOKEN_PROGRAM_ID: ProgramId = [16; 8];
|
||||
|
||||
fn admin_id() -> AccountId {
|
||||
AccountId::new([9; 32])
|
||||
}
|
||||
|
||||
fn config_init() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: AMM_PROGRAM_ID,
|
||||
balance: 0,
|
||||
data: Data::from(&AmmConfig {
|
||||
token_program_id: TOKEN_PROGRAM_ID,
|
||||
authority: admin_id(),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
is_authorized: false,
|
||||
account_id: compute_config_pda(AMM_PROGRAM_ID),
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_authorized() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: true,
|
||||
account_id: admin_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn updated_config(post_states: &[AccountPostState]) -> AmmConfig {
|
||||
AmmConfig::try_from(&post_states[0].account().data)
|
||||
.expect("post state must contain a valid AmmConfig")
|
||||
}
|
||||
|
||||
// ── happy path ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn updates_token_program_id() {
|
||||
let post_states = update_config(
|
||||
config_init(),
|
||||
admin_authorized(),
|
||||
Some(NEW_TOKEN_PROGRAM_ID),
|
||||
None,
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
let config = updated_config(&post_states);
|
||||
assert_eq!(config.token_program_id, NEW_TOKEN_PROGRAM_ID);
|
||||
// Authority is unchanged.
|
||||
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,
|
||||
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),
|
||||
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,
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
let config = updated_config(&post_states);
|
||||
assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID);
|
||||
assert_eq!(config.authority, admin_id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_config_and_echoed_authority_post_states() {
|
||||
let authority = admin_authorized();
|
||||
let post_states = update_config(
|
||||
config_init(),
|
||||
authority.clone(),
|
||||
Some(NEW_TOKEN_PROGRAM_ID),
|
||||
None,
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
assert_eq!(post_states.len(), 2);
|
||||
// The config keeps its program owner (it is updated in place, not claimed).
|
||||
assert_eq!(post_states[0].account().program_owner, AMM_PROGRAM_ID);
|
||||
assert_eq!(*post_states[1].account(), authority.account);
|
||||
}
|
||||
|
||||
// ── precondition violations ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "AMM config Account ID does not match PDA")]
|
||||
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,
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "AMM Program must be initialized before use")]
|
||||
fn uninitialized_config_panics() {
|
||||
let config = AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: false,
|
||||
account_id: compute_config_pda(AMM_PROGRAM_ID),
|
||||
};
|
||||
update_config(
|
||||
config,
|
||||
admin_authorized(),
|
||||
Some(NEW_TOKEN_PROGRAM_ID),
|
||||
None,
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
/// A caller who is not the configured admin cannot change the config, even if they sign.
|
||||
#[test]
|
||||
#[should_panic(expected = "caller is not the configured admin authority")]
|
||||
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,
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
/// The admin account must actually sign; passing it unauthorized is rejected.
|
||||
#[test]
|
||||
#[should_panic(expected = "admin authority must authorize the update")]
|
||||
fn unauthorized_admin_panics() {
|
||||
let mut unsigned = admin_authorized();
|
||||
unsigned.is_authorized = false;
|
||||
update_config(
|
||||
config_init(),
|
||||
unsigned,
|
||||
Some(NEW_TOKEN_PROGRAM_ID),
|
||||
None,
|
||||
AMM_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,10 @@ impl Keys {
|
||||
fn user_lp() -> PrivateKey {
|
||||
PrivateKey::try_new([33; 32]).expect("valid private key")
|
||||
}
|
||||
|
||||
fn admin() -> PrivateKey {
|
||||
PrivateKey::try_new([34; 32]).expect("valid private key")
|
||||
}
|
||||
}
|
||||
|
||||
impl Ids {
|
||||
@ -98,6 +102,10 @@ impl Ids {
|
||||
fn user_lp() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::user_lp()))
|
||||
}
|
||||
|
||||
fn admin() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::admin()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Balances {
|
||||
@ -293,6 +301,7 @@ impl Accounts {
|
||||
balance: 0_u128,
|
||||
data: Data::from(&amm_core::AmmConfig {
|
||||
token_program_id: Ids::token_program(),
|
||||
authority: Ids::admin(),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
@ -1180,6 +1189,7 @@ fn execute_remove_liquidity(
|
||||
fn execute_initialize(state: &mut V03State) {
|
||||
let instruction = amm_core::Instruction::Initialize {
|
||||
token_program_id: Ids::token_program(),
|
||||
authority: Ids::admin(),
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
@ -1241,10 +1251,102 @@ fn amm_initialize_creates_config_account() {
|
||||
let config_account = state.get_account_by_id(Ids::config());
|
||||
assert_eq!(config_account, Accounts::config());
|
||||
|
||||
// Explicitly assert the stored Token Program ID round-trips from the instruction argument.
|
||||
// Explicitly assert the stored Token Program ID and admin authority round-trip from the
|
||||
// instruction arguments.
|
||||
let config = amm_core::AmmConfig::try_from(&config_account.data)
|
||||
.expect("config account must hold a valid AmmConfig");
|
||||
assert_eq!(config.token_program_id, Ids::token_program());
|
||||
assert_eq!(config.authority, Ids::admin());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn execute_update_config(
|
||||
state: &mut V03State,
|
||||
signer: &PrivateKey,
|
||||
token_program_id: Option<nssa_core::program::ProgramId>,
|
||||
new_authority: Option<AccountId>,
|
||||
) -> Result<(), NssaError> {
|
||||
let signer_id = AccountId::from(&PublicKey::new_from_private_key(signer));
|
||||
let instruction = amm_core::Instruction::UpdateConfig {
|
||||
token_program_id,
|
||||
new_authority,
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::amm_program(),
|
||||
vec![Ids::config(), signer_id],
|
||||
vec![current_nonce(state, signer_id)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[signer]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state.transition_from_public_transaction(&tx, 0, 0)
|
||||
}
|
||||
|
||||
fn config_data(state: &V03State) -> amm_core::AmmConfig {
|
||||
amm_core::AmmConfig::try_from(&state.get_account_by_id(Ids::config()).data)
|
||||
.expect("config account must hold a valid AmmConfig")
|
||||
}
|
||||
|
||||
fn initialized_amm_state() -> V03State {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
deploy_programs(&mut state);
|
||||
execute_initialize(&mut state);
|
||||
state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amm_update_config_changes_token_program_id_and_authority() {
|
||||
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),
|
||||
Some(new_admin),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = config_data(&state);
|
||||
assert_eq!(config.token_program_id, new_token_program);
|
||||
assert_eq!(config.authority, new_admin);
|
||||
}
|
||||
|
||||
#[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
|
||||
// left unchanged.
|
||||
let result = execute_update_config(&mut state, &Keys::user_a(), Some([123u32; 8]), None);
|
||||
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
||||
|
||||
let config = config_data(&state);
|
||||
assert_eq!(config.token_program_id, Ids::token_program());
|
||||
assert_eq!(config.authority, Ids::admin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amm_update_config_authority_handoff_revokes_old_admin() {
|
||||
let mut state = initialized_amm_state();
|
||||
let new_admin = Ids::user_a();
|
||||
|
||||
// Admin hands off control to user_a.
|
||||
execute_update_config(&mut state, &Keys::admin(), None, Some(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);
|
||||
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
||||
|
||||
// The new admin can.
|
||||
execute_update_config(&mut state, &Keys::user_a(), Some([124u32; 8]), None).unwrap();
|
||||
assert_eq!(config_data(&state).token_program_id, [124u32; 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user