Files
lez-programs/programs/stablecoin/src/test_support.rs
T

219 lines
7.7 KiB
Rust

//! Shared account builders for the poke host-function unit tests.
//!
//! The three pokes (`accrue_stability_fee`, `update_redemption_rate`,
//! `refresh_globals`) validate overlapping account sets, so their tests would
//! otherwise repeat the same fixtures three times.
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
program::ProgramId,
};
use stablecoin_core::{
compute_protocol_parameters_pda, compute_redemption_price_state_pda,
compute_stability_fee_accumulator_pda, math::FIXED_POINT_ONE, ProtocolParameters,
RedemptionPriceState, StabilityFeeAccumulator,
};
use twap_oracle_core::OraclePriceAccount;
pub(crate) const STABLECOIN_PROGRAM_ID: ProgramId = [3u32; 8];
pub(crate) const ORACLE_PROGRAM_ID: ProgramId = [4u32; 8];
pub(crate) const CLOCK_PROGRAM_ID: ProgramId = [5u32; 8];
/// Timestamps are Unix milliseconds, matching the `CLOCK_01` account.
pub(crate) const T0: u64 = 1_700_000_000_000;
/// Ten minutes after `T0` — past the 300_000 ms rate-update interval below.
pub(crate) const NOW: u64 = T0 + 600_000;
/// ~5% annual in per-millisecond fixed-point, the same realistic rate the
/// `initialize_program` tests use.
pub(crate) const TEST_STABILITY_FEE_PER_MILLISECOND: u128 = FIXED_POINT_ONE + 1_500_000_000_000_000;
pub(crate) const TEST_MINIMUM_MILLISECONDS_BETWEEN_RATE_UPDATES: u64 = 300_000;
pub(crate) const TEST_MAXIMUM_ORACLE_PRICE_AGE_MILLISECONDS: u64 = 900_000;
/// Starting accumulator anchor: `1.0`.
pub(crate) const ACCUMULATOR_ANCHOR: u128 = FIXED_POINT_ONE;
/// Starting redemption price: `0.5` collateral per stablecoin.
pub(crate) const REDEMPTION_PRICE_ANCHOR: u128 = FIXED_POINT_ONE / 2;
/// A market price below the redemption anchor, so `error > 0` and a positive
/// `Kp` drives the rate above `FIXED_POINT_ONE`.
pub(crate) const MARKET_PRICE_BELOW_ANCHOR: u128 = FIXED_POINT_ONE / 4;
/// Knobs the poke tests actually vary. Everything else is fixed.
pub(crate) struct ParameterOverrides {
pub(crate) stability_fee_per_millisecond: u128,
pub(crate) controller_proportional_gain: i128,
pub(crate) controller_integral_gain: i128,
pub(crate) is_frozen: bool,
}
impl Default for ParameterOverrides {
fn default() -> Self {
Self {
stability_fee_per_millisecond: TEST_STABILITY_FEE_PER_MILLISECOND,
controller_proportional_gain: FIXED_POINT_ONE as i128,
controller_integral_gain: 0,
is_frozen: false,
}
}
}
pub(crate) fn caller_id() -> AccountId {
AccountId::new([0xCA; 32])
}
pub(crate) fn admin_id() -> AccountId {
AccountId::new([0xA0; 32])
}
pub(crate) fn freeze_authority_id() -> AccountId {
AccountId::new([0xFE; 32])
}
pub(crate) fn stablecoin_definition_id() -> AccountId {
AccountId::new([0x10; 32])
}
pub(crate) fn collateral_definition_id() -> AccountId {
AccountId::new([0x20; 32])
}
pub(crate) fn oracle_id() -> AccountId {
AccountId::new([0x30; 32])
}
pub(crate) fn oracle_source_id() -> AccountId {
AccountId::new([0x31; 32])
}
pub(crate) fn protocol_parameters_id() -> AccountId {
compute_protocol_parameters_pda(STABLECOIN_PROGRAM_ID)
}
pub(crate) fn accumulator_id() -> AccountId {
compute_stability_fee_accumulator_pda(STABLECOIN_PROGRAM_ID)
}
pub(crate) fn redemption_price_state_id() -> AccountId {
compute_redemption_price_state_pda(STABLECOIN_PROGRAM_ID)
}
/// An authorized caller. Pokes are permissionless, so any signer works.
pub(crate) fn caller_account() -> AccountWithMetadata {
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: caller_id(),
}
}
/// An uninitialized account at `account_id` — the shape every "must be
/// initialized" assertion rejects.
pub(crate) fn uninitialized(account_id: AccountId) -> AccountWithMetadata {
AccountWithMetadata {
account: Account::default(),
is_authorized: false,
account_id,
}
}
pub(crate) fn protocol_parameters_account(overrides: ParameterOverrides) -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: STABLECOIN_PROGRAM_ID,
balance: 0,
data: Data::from(&ProtocolParameters {
admin_account_id: admin_id(),
freeze_authority_account_id: freeze_authority_id(),
stablecoin_definition_id: stablecoin_definition_id(),
collateral_definition_id: collateral_definition_id(),
market_price_oracle_id: oracle_id(),
stability_fee_per_millisecond: overrides.stability_fee_per_millisecond,
controller_proportional_gain: overrides.controller_proportional_gain,
controller_integral_gain: overrides.controller_integral_gain,
minimum_collateralization_ratio: FIXED_POINT_ONE * 3 / 2,
minimum_milliseconds_between_rate_updates:
TEST_MINIMUM_MILLISECONDS_BETWEEN_RATE_UPDATES,
maximum_oracle_price_age_milliseconds: TEST_MAXIMUM_ORACLE_PRICE_AGE_MILLISECONDS,
is_frozen: overrides.is_frozen,
}),
nonce: Nonce(0),
},
is_authorized: false,
account_id: protocol_parameters_id(),
}
}
pub(crate) fn accumulator_account(anchor: u128, last_accrued_at: u64) -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: STABLECOIN_PROGRAM_ID,
balance: 0,
data: Data::from(&StabilityFeeAccumulator {
accumulated_rate_at_last_accrual: anchor,
last_accrued_at,
}),
nonce: Nonce(0),
},
is_authorized: false,
account_id: accumulator_id(),
}
}
pub(crate) fn redemption_price_state_account(last_updated_at: u64) -> AccountWithMetadata {
redemption_price_state_account_with_integral_term(last_updated_at, 0)
}
pub(crate) fn redemption_price_state_account_with_integral_term(
last_updated_at: u64,
controller_integral_term: i128,
) -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: STABLECOIN_PROGRAM_ID,
balance: 0,
data: Data::from(&RedemptionPriceState {
redemption_price_at_last_update: REDEMPTION_PRICE_ANCHOR,
redemption_rate_per_millisecond: FIXED_POINT_ONE,
controller_integral_term,
last_updated_at,
}),
nonce: Nonce(0),
},
is_authorized: false,
account_id: redemption_price_state_id(),
}
}
pub(crate) fn oracle_account(timestamp: u64, price: u128) -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ORACLE_PROGRAM_ID,
balance: 0,
data: Data::from(&OraclePriceAccount {
base_asset: stablecoin_definition_id(),
quote_asset: collateral_definition_id(),
price,
timestamp,
source_id: oracle_source_id(),
confidence_interval: 0,
}),
nonce: Nonce(0),
},
is_authorized: false,
account_id: oracle_id(),
}
}
pub(crate) fn clock_account(timestamp: u64) -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: CLOCK_PROGRAM_ID,
balance: 0,
data: Data::try_from(
ClockAccountData {
block_id: 0,
timestamp,
}
.to_bytes(),
)
.expect("clock data fits"),
nonce: Nonce(0),
},
is_authorized: false,
account_id: CLOCK_01_PROGRAM_ACCOUNT_ID,
}
}