lez-programs/programs/stablecoin/src/initialize_program.rs
2026-07-02 17:24:38 -03:00

207 lines
7.9 KiB
Rust

use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::{AccountPostState, ChainedCall, Claim, ProgramId},
};
use stablecoin_core::{
assert_protocol_parameter_bounds, compute_protocol_parameters_pda,
compute_protocol_parameters_pda_seed, compute_redemption_price_state_pda,
compute_redemption_price_state_pda_seed, compute_stability_fee_accumulator_pda,
compute_stability_fee_accumulator_pda_seed, compute_stablecoin_definition_pda,
compute_stablecoin_definition_pda_seed, compute_stablecoin_master_holding_pda,
compute_stablecoin_master_holding_pda_seed, ProtocolParameters, RedemptionPriceState,
StabilityFeeAccumulator, FIXED_POINT_ONE,
};
use token_core::TokenDefinition;
use twap_oracle_core::OraclePriceAccount;
use crate::shared::read_clock_timestamp;
/// Initializes stablecoin protocol globals and creates the stablecoin token definition.
///
/// # Panics
/// Panics if any account is not the expected PDA/state shape or any parameter is out of bounds.
#[expect(
clippy::too_many_arguments,
reason = "instruction surface initializes all protocol singleton accounts in one call"
)]
pub fn initialize_program(
admin: AccountWithMetadata,
protocol_parameters: AccountWithMetadata,
stability_fee_accumulator: AccountWithMetadata,
redemption_price_state: AccountWithMetadata,
stablecoin_definition: AccountWithMetadata,
stablecoin_master_holding: AccountWithMetadata,
collateral_definition: AccountWithMetadata,
market_price_oracle: AccountWithMetadata,
clock: AccountWithMetadata,
stablecoin_program_id: ProgramId,
freeze_authority_account_id: nssa_core::account::AccountId,
initial_stability_fee_per_millisecond: u128,
initial_controller_proportional_gain: i128,
initial_controller_integral_gain: i128,
initial_minimum_collateralization_ratio: u128,
minimum_milliseconds_between_rate_updates: u64,
maximum_oracle_price_age_milliseconds: u64,
initial_redemption_price: u128,
stablecoin_name: String,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
assert!(admin.is_authorized, "Admin authorization is missing");
assert_protocol_parameter_bounds(
initial_stability_fee_per_millisecond,
initial_controller_proportional_gain,
initial_controller_integral_gain,
initial_minimum_collateralization_ratio,
minimum_milliseconds_between_rate_updates,
maximum_oracle_price_age_milliseconds,
initial_redemption_price,
);
assert_uninitialized_pda(
&protocol_parameters,
compute_protocol_parameters_pda(stablecoin_program_id),
"Protocol parameters",
);
assert_uninitialized_pda(
&stability_fee_accumulator,
compute_stability_fee_accumulator_pda(stablecoin_program_id),
"Stability fee accumulator",
);
assert_uninitialized_pda(
&redemption_price_state,
compute_redemption_price_state_pda(stablecoin_program_id),
"Redemption price state",
);
assert_uninitialized_pda(
&stablecoin_definition,
compute_stablecoin_definition_pda(stablecoin_program_id),
"Stablecoin definition",
);
assert_uninitialized_pda(
&stablecoin_master_holding,
compute_stablecoin_master_holding_pda(stablecoin_program_id),
"Stablecoin master holding",
);
assert_ne!(
collateral_definition.account,
Account::default(),
"Collateral definition account must be initialized"
);
let collateral_definition_data = TokenDefinition::try_from(&collateral_definition.account.data)
.expect("Collateral definition account must hold a valid TokenDefinition");
assert!(
matches!(collateral_definition_data, TokenDefinition::Fungible { .. }),
"Collateral definition must be fungible"
);
assert_ne!(
market_price_oracle.account,
Account::default(),
"Market price oracle account must be initialized"
);
let oracle = OraclePriceAccount::try_from(&market_price_oracle.account.data)
.expect("Market price oracle account must hold a valid OraclePriceAccount");
assert_eq!(
oracle.base_asset, stablecoin_definition.account_id,
"Market price oracle base asset must be the stablecoin definition"
);
assert_eq!(
oracle.quote_asset, collateral_definition.account_id,
"Market price oracle quote asset must be the collateral definition"
);
let now = read_clock_timestamp(&clock);
let protocol_data = ProtocolParameters {
admin_account_id: admin.account_id,
freeze_authority_account_id,
stablecoin_definition_id: stablecoin_definition.account_id,
collateral_definition_id: collateral_definition.account_id,
market_price_oracle_id: market_price_oracle.account_id,
stability_fee_per_millisecond: initial_stability_fee_per_millisecond,
controller_proportional_gain: initial_controller_proportional_gain,
controller_integral_gain: initial_controller_integral_gain,
minimum_collateralization_ratio: initial_minimum_collateralization_ratio,
minimum_milliseconds_between_rate_updates,
maximum_oracle_price_age_milliseconds,
is_frozen: false,
};
let mut protocol_post = protocol_parameters.account.clone();
protocol_post.data = Data::from(&protocol_data);
let mut fee_post = stability_fee_accumulator.account.clone();
fee_post.data = Data::from(&StabilityFeeAccumulator {
accumulated_rate_at_last_accrual: FIXED_POINT_ONE,
last_accrued_at: now,
});
let mut redemption_post = redemption_price_state.account.clone();
redemption_post.data = Data::from(&RedemptionPriceState {
redemption_price_at_last_update: initial_redemption_price,
redemption_rate_per_millisecond: FIXED_POINT_ONE,
controller_integral_term: 0,
last_updated_at: now,
});
let mut stablecoin_definition_authorized = stablecoin_definition.clone();
stablecoin_definition_authorized.is_authorized = true;
let mut stablecoin_master_holding_authorized = stablecoin_master_holding.clone();
stablecoin_master_holding_authorized.is_authorized = true;
let token_program_id = collateral_definition.account.program_owner;
let create_stablecoin_call = ChainedCall::new(
token_program_id,
vec![
stablecoin_definition_authorized,
stablecoin_master_holding_authorized,
],
&token_core::Instruction::NewFungibleDefinition {
name: stablecoin_name,
total_supply: 0,
mint_authority: Some(stablecoin_definition.account_id),
},
)
.with_pda_seeds(vec![
compute_stablecoin_definition_pda_seed(),
compute_stablecoin_master_holding_pda_seed(),
]);
let post_states = vec![
AccountPostState::new(admin.account),
AccountPostState::new_claimed(
protocol_post,
Claim::Pda(compute_protocol_parameters_pda_seed()),
),
AccountPostState::new_claimed(
fee_post,
Claim::Pda(compute_stability_fee_accumulator_pda_seed()),
),
AccountPostState::new_claimed(
redemption_post,
Claim::Pda(compute_redemption_price_state_pda_seed()),
),
AccountPostState::new(stablecoin_definition.account),
AccountPostState::new(stablecoin_master_holding.account),
AccountPostState::new(collateral_definition.account),
AccountPostState::new(market_price_oracle.account),
AccountPostState::new(clock.account),
];
(post_states, vec![create_stablecoin_call])
}
fn assert_uninitialized_pda(
account: &AccountWithMetadata,
expected_id: nssa_core::account::AccountId,
label: &str,
) {
assert_eq!(
account.account_id, expected_id,
"{label} account ID does not match PDA"
);
assert_eq!(
account.account,
Account::default(),
"{label} account must be uninitialized"
);
}