2026-08-18 15:33:33 +00:00
|
|
|
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
|
2026-05-22 11:09:13 +02:00
|
|
|
use nssa::{
|
|
|
|
|
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
|
|
|
|
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
|
|
|
|
};
|
|
|
|
|
use nssa_core::account::{Account, AccountId, Data, Nonce};
|
2026-08-18 15:33:33 +00:00
|
|
|
use stablecoin_core::{
|
|
|
|
|
compute_position_pda, compute_position_vault_pda, compute_protocol_parameters_pda,
|
|
|
|
|
compute_redemption_price_state_pda, compute_stability_fee_accumulator_pda,
|
|
|
|
|
compute_stablecoin_definition_pda, compute_stablecoin_master_holding_pda, Position,
|
|
|
|
|
};
|
2026-05-22 11:09:13 +02:00
|
|
|
use token_core::{TokenDefinition, TokenHolding};
|
|
|
|
|
|
|
|
|
|
struct Keys;
|
|
|
|
|
struct Ids;
|
|
|
|
|
struct Balances;
|
|
|
|
|
struct Accounts;
|
|
|
|
|
|
|
|
|
|
impl Keys {
|
|
|
|
|
fn owner() -> PrivateKey {
|
|
|
|
|
PrivateKey::try_new([41; 32]).expect("valid private key")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_holding() -> PrivateKey {
|
|
|
|
|
PrivateKey::try_new([42; 32]).expect("valid private key")
|
|
|
|
|
}
|
2026-05-22 11:09:16 +02:00
|
|
|
|
|
|
|
|
fn user_stablecoin_holding() -> PrivateKey {
|
|
|
|
|
PrivateKey::try_new([43; 32]).expect("valid private key")
|
|
|
|
|
}
|
2026-08-18 15:33:33 +00:00
|
|
|
|
|
|
|
|
fn admin() -> PrivateKey {
|
|
|
|
|
PrivateKey::try_new([44; 32]).expect("valid private key")
|
|
|
|
|
}
|
2026-05-22 11:09:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Ids {
|
|
|
|
|
fn token_program() -> nssa_core::program::ProgramId {
|
|
|
|
|
token_methods::TOKEN_ID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stablecoin_program() -> nssa_core::program::ProgramId {
|
|
|
|
|
stablecoin_methods::STABLECOIN_ID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collateral_definition() -> AccountId {
|
|
|
|
|
AccountId::new([5; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn owner() -> AccountId {
|
|
|
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::owner()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_holding() -> AccountId {
|
|
|
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::user_holding()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 11:09:16 +02:00
|
|
|
fn stablecoin_definition() -> AccountId {
|
|
|
|
|
AccountId::new([6; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_stablecoin_holding() -> AccountId {
|
|
|
|
|
AccountId::from(&PublicKey::new_from_private_key(
|
|
|
|
|
&Keys::user_stablecoin_holding(),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 15:33:32 +00:00
|
|
|
fn position_nonce() -> u64 {
|
|
|
|
|
0
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 11:09:13 +02:00
|
|
|
fn position() -> AccountId {
|
|
|
|
|
compute_position_pda(
|
|
|
|
|
Self::stablecoin_program(),
|
|
|
|
|
Self::owner(),
|
2026-08-18 15:33:32 +00:00
|
|
|
Self::position_nonce(),
|
2026-05-22 11:09:13 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn vault() -> AccountId {
|
|
|
|
|
compute_position_vault_pda(Self::stablecoin_program(), Self::position())
|
|
|
|
|
}
|
2026-08-18 15:33:33 +00:00
|
|
|
|
|
|
|
|
fn admin() -> AccountId {
|
|
|
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::admin()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn freeze_authority() -> AccountId {
|
|
|
|
|
AccountId::new([0xFE; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn oracle() -> AccountId {
|
|
|
|
|
AccountId::new([0x70; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The stablecoin's `TokenDefinition` PDA created by `initialize_program`
|
|
|
|
|
/// (distinct from `stablecoin_definition`, the externally-owned definition
|
|
|
|
|
/// the repay test uses).
|
|
|
|
|
fn stablecoin_definition_pda() -> AccountId {
|
|
|
|
|
compute_stablecoin_definition_pda(Self::stablecoin_program())
|
|
|
|
|
}
|
2026-05-22 11:09:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Balances {
|
|
|
|
|
fn user_holding_init() -> u128 {
|
|
|
|
|
1_000_000
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collateral_deposit() -> u128 {
|
|
|
|
|
500_000
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collateral_withdraw() -> u128 {
|
|
|
|
|
200_000
|
|
|
|
|
}
|
2026-05-22 11:09:16 +02:00
|
|
|
|
|
|
|
|
fn stablecoin_supply_init() -> u128 {
|
|
|
|
|
1_000
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_stablecoin_holding_init() -> u128 {
|
|
|
|
|
1_000
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn initial_debt() -> u128 {
|
|
|
|
|
300
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn debt_repay_amount() -> u128 {
|
|
|
|
|
100
|
|
|
|
|
}
|
2026-05-22 11:09:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Accounts {
|
|
|
|
|
fn collateral_definition_init() -> Account {
|
|
|
|
|
Account {
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
name: String::from("Gold"),
|
|
|
|
|
total_supply: Balances::user_holding_init(),
|
|
|
|
|
metadata_id: None,
|
2026-05-27 15:04:28 +05:30
|
|
|
authority: None,
|
2026-05-22 11:09:13 +02:00
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_holding_init() -> Account {
|
|
|
|
|
Account {
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
definition_id: Ids::collateral_definition(),
|
|
|
|
|
balance: Balances::user_holding_init(),
|
|
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-22 11:09:16 +02:00
|
|
|
|
|
|
|
|
fn stablecoin_definition_init() -> Account {
|
|
|
|
|
Account {
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
name: String::from("DAI"),
|
|
|
|
|
total_supply: Balances::stablecoin_supply_init(),
|
|
|
|
|
metadata_id: None,
|
2026-05-27 15:04:28 +05:30
|
|
|
authority: None,
|
2026-05-22 11:09:16 +02:00
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_stablecoin_holding_init() -> Account {
|
|
|
|
|
Account {
|
|
|
|
|
program_owner: Ids::token_program(),
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
definition_id: Ids::stablecoin_definition(),
|
|
|
|
|
balance: Balances::user_stablecoin_holding_init(),
|
|
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn position_with_debt_init() -> Account {
|
|
|
|
|
Account {
|
|
|
|
|
program_owner: stablecoin_methods::STABLECOIN_ID,
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
data: Data::from(&Position {
|
2026-08-18 15:33:32 +00:00
|
|
|
owner_account_id: Ids::owner(),
|
|
|
|
|
position_nonce: Ids::position_nonce(),
|
|
|
|
|
vault_account_id: Ids::vault(),
|
2026-05-22 11:09:16 +02:00
|
|
|
collateral_amount: Balances::collateral_deposit(),
|
2026-08-18 15:33:32 +00:00
|
|
|
normalized_debt_amount: Balances::initial_debt(),
|
|
|
|
|
opened_at: 0,
|
2026-05-22 11:09:16 +02:00
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-18 15:33:33 +00:00
|
|
|
|
|
|
|
|
fn oracle_init(base_asset: AccountId, quote_asset: AccountId) -> Account {
|
|
|
|
|
Account {
|
|
|
|
|
program_owner: [9u32; 8],
|
|
|
|
|
balance: 0_u128,
|
|
|
|
|
data: Data::from(&twap_oracle_core::OraclePriceAccount {
|
|
|
|
|
base_asset,
|
|
|
|
|
quote_asset,
|
|
|
|
|
price: stablecoin_core::math::FIXED_POINT_ONE / 2,
|
|
|
|
|
timestamp: 0,
|
|
|
|
|
source_id: Ids::oracle(),
|
|
|
|
|
confidence_interval: 0,
|
|
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Seeds the canonical `CLOCK_01` account at `timestamp`. `V03State::new()` no longer
|
|
|
|
|
/// auto-creates it, and `initialize_program` reads it for wall-clock time.
|
|
|
|
|
fn seed_clock(state: &mut V03State, timestamp: u64) {
|
|
|
|
|
let data = ClockAccountData {
|
|
|
|
|
block_id: 0,
|
|
|
|
|
timestamp,
|
|
|
|
|
}
|
|
|
|
|
.to_bytes();
|
|
|
|
|
let clock_account = Account {
|
|
|
|
|
data: Data::try_from(data).expect("clock account data fits"),
|
|
|
|
|
..Account::default()
|
|
|
|
|
};
|
|
|
|
|
state.force_insert_account(CLOCK_01_PROGRAM_ACCOUNT_ID, clock_account);
|
2026-05-22 11:09:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn deploy_programs(state: &mut V03State) {
|
|
|
|
|
let token_message =
|
|
|
|
|
program_deployment_transaction::Message::new(token_methods::TOKEN_ELF.to_vec());
|
|
|
|
|
state
|
|
|
|
|
.transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new(
|
|
|
|
|
token_message,
|
|
|
|
|
))
|
|
|
|
|
.expect("token program deployment must succeed");
|
|
|
|
|
|
|
|
|
|
let stablecoin_message =
|
|
|
|
|
program_deployment_transaction::Message::new(stablecoin_methods::STABLECOIN_ELF.to_vec());
|
|
|
|
|
state
|
|
|
|
|
.transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new(
|
|
|
|
|
stablecoin_message,
|
|
|
|
|
))
|
|
|
|
|
.expect("stablecoin program deployment must succeed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn state_for_stablecoin_tests() -> V03State {
|
2026-06-29 01:16:34 +02:00
|
|
|
let mut state = V03State::new();
|
2026-05-22 11:09:13 +02:00
|
|
|
deploy_programs(&mut state);
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
Ids::collateral_definition(),
|
|
|
|
|
Accounts::collateral_definition_init(),
|
|
|
|
|
);
|
|
|
|
|
state.force_insert_account(Ids::user_holding(), Accounts::user_holding_init());
|
|
|
|
|
state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn current_nonce(state: &V03State, account_id: AccountId) -> Nonce {
|
|
|
|
|
state.get_account_by_id(account_id).nonce
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 11:09:16 +02:00
|
|
|
fn state_for_stablecoin_repay_tests() -> V03State {
|
2026-06-29 01:16:34 +02:00
|
|
|
let mut state = V03State::new();
|
2026-05-22 11:09:16 +02:00
|
|
|
deploy_programs(&mut state);
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
Ids::collateral_definition(),
|
|
|
|
|
Accounts::collateral_definition_init(),
|
|
|
|
|
);
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
Ids::stablecoin_definition(),
|
|
|
|
|
Accounts::stablecoin_definition_init(),
|
|
|
|
|
);
|
|
|
|
|
state.force_insert_account(Ids::position(), Accounts::position_with_debt_init());
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
Ids::user_stablecoin_holding(),
|
|
|
|
|
Accounts::user_stablecoin_holding_init(),
|
|
|
|
|
);
|
|
|
|
|
state
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 11:09:13 +02:00
|
|
|
fn assert_position(state: &V03State, expected_collateral: u128) {
|
2026-05-22 11:09:16 +02:00
|
|
|
let position =
|
|
|
|
|
Position::try_from(&state.get_account_by_id(Ids::position()).data).expect("valid Position");
|
2026-05-22 11:09:13 +02:00
|
|
|
assert_eq!(position.collateral_amount, expected_collateral);
|
2026-08-18 15:33:32 +00:00
|
|
|
assert_eq!(position.normalized_debt_amount, 0);
|
|
|
|
|
assert_eq!(position.vault_account_id, Ids::vault());
|
|
|
|
|
assert_eq!(position.owner_account_id, Ids::owner());
|
|
|
|
|
assert_eq!(position.position_nonce, Ids::position_nonce());
|
2026-05-22 11:09:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_fungible_balance(state: &V03State, account_id: AccountId, expected_balance: u128) {
|
|
|
|
|
let holding = TokenHolding::try_from(&state.get_account_by_id(account_id).data)
|
|
|
|
|
.expect("valid TokenHolding");
|
|
|
|
|
match holding {
|
|
|
|
|
TokenHolding::Fungible {
|
|
|
|
|
definition_id,
|
|
|
|
|
balance,
|
|
|
|
|
} => {
|
|
|
|
|
assert_eq!(definition_id, Ids::collateral_definition());
|
|
|
|
|
assert_eq!(balance, expected_balance);
|
|
|
|
|
}
|
|
|
|
|
TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => {
|
|
|
|
|
panic!("expected Fungible holding")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn stablecoin_open_position_then_withdraw_collateral() {
|
|
|
|
|
let mut state = state_for_stablecoin_tests();
|
|
|
|
|
|
|
|
|
|
// Open the position: deposit collateral from the user's holding into a fresh vault.
|
|
|
|
|
let open = stablecoin_core::Instruction::OpenPosition {
|
2026-08-18 15:33:32 +00:00
|
|
|
position_nonce: Ids::position_nonce(),
|
2026-05-22 11:09:13 +02:00
|
|
|
collateral_amount: Balances::collateral_deposit(),
|
|
|
|
|
};
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
Ids::stablecoin_program(),
|
|
|
|
|
vec![
|
|
|
|
|
Ids::owner(),
|
|
|
|
|
Ids::position(),
|
|
|
|
|
Ids::vault(),
|
|
|
|
|
Ids::user_holding(),
|
|
|
|
|
Ids::collateral_definition(),
|
|
|
|
|
],
|
|
|
|
|
vec![
|
|
|
|
|
current_nonce(&state, Ids::owner()),
|
|
|
|
|
current_nonce(&state, Ids::user_holding()),
|
|
|
|
|
],
|
|
|
|
|
open,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(
|
|
|
|
|
&message,
|
|
|
|
|
&[&Keys::owner(), &Keys::user_holding()],
|
|
|
|
|
);
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
state
|
|
|
|
|
.transition_from_public_transaction(&tx, 0, 0)
|
|
|
|
|
.expect("open_position must succeed");
|
|
|
|
|
|
|
|
|
|
assert_position(&state, Balances::collateral_deposit());
|
|
|
|
|
assert_fungible_balance(&state, Ids::vault(), Balances::collateral_deposit());
|
|
|
|
|
assert_fungible_balance(
|
|
|
|
|
&state,
|
|
|
|
|
Ids::user_holding(),
|
|
|
|
|
Balances::user_holding_init() - Balances::collateral_deposit(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Withdraw part of the collateral back to the same user holding.
|
|
|
|
|
let withdraw = stablecoin_core::Instruction::WithdrawCollateral {
|
|
|
|
|
amount: Balances::collateral_withdraw(),
|
|
|
|
|
};
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
Ids::stablecoin_program(),
|
|
|
|
|
vec![
|
|
|
|
|
Ids::owner(),
|
|
|
|
|
Ids::position(),
|
|
|
|
|
Ids::vault(),
|
|
|
|
|
Ids::user_holding(),
|
|
|
|
|
],
|
|
|
|
|
vec![current_nonce(&state, Ids::owner())],
|
|
|
|
|
withdraw,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2026-05-22 11:09:16 +02:00
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner()]);
|
2026-05-22 11:09:13 +02:00
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
state
|
|
|
|
|
.transition_from_public_transaction(&tx, 0, 0)
|
|
|
|
|
.expect("withdraw_collateral must succeed");
|
|
|
|
|
|
|
|
|
|
assert_position(
|
|
|
|
|
&state,
|
|
|
|
|
Balances::collateral_deposit() - Balances::collateral_withdraw(),
|
|
|
|
|
);
|
|
|
|
|
assert_fungible_balance(
|
|
|
|
|
&state,
|
|
|
|
|
Ids::vault(),
|
|
|
|
|
Balances::collateral_deposit() - Balances::collateral_withdraw(),
|
|
|
|
|
);
|
|
|
|
|
assert_fungible_balance(
|
|
|
|
|
&state,
|
|
|
|
|
Ids::user_holding(),
|
|
|
|
|
Balances::user_holding_init() - Balances::collateral_deposit()
|
|
|
|
|
+ Balances::collateral_withdraw(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-22 11:09:16 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn stablecoin_repay_debt_burns_stablecoins_and_decreases_debt() {
|
|
|
|
|
let mut state = state_for_stablecoin_repay_tests();
|
|
|
|
|
|
|
|
|
|
let repay = stablecoin_core::Instruction::RepayDebt {
|
|
|
|
|
amount: Balances::debt_repay_amount(),
|
|
|
|
|
};
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
Ids::stablecoin_program(),
|
|
|
|
|
vec![
|
|
|
|
|
Ids::owner(),
|
|
|
|
|
Ids::position(),
|
|
|
|
|
Ids::stablecoin_definition(),
|
|
|
|
|
Ids::user_stablecoin_holding(),
|
|
|
|
|
],
|
|
|
|
|
vec![
|
|
|
|
|
current_nonce(&state, Ids::owner()),
|
|
|
|
|
current_nonce(&state, Ids::user_stablecoin_holding()),
|
|
|
|
|
],
|
|
|
|
|
repay,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(
|
|
|
|
|
&message,
|
|
|
|
|
&[&Keys::owner(), &Keys::user_stablecoin_holding()],
|
|
|
|
|
);
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
state
|
|
|
|
|
.transition_from_public_transaction(&tx, 0, 0)
|
|
|
|
|
.expect("repay_debt must succeed");
|
|
|
|
|
|
|
|
|
|
// Position debt decreased; collateral untouched.
|
|
|
|
|
let position =
|
|
|
|
|
Position::try_from(&state.get_account_by_id(Ids::position()).data).expect("valid Position");
|
|
|
|
|
assert_eq!(
|
2026-08-18 15:33:32 +00:00
|
|
|
position.normalized_debt_amount,
|
2026-05-22 11:09:16 +02:00
|
|
|
Balances::initial_debt() - Balances::debt_repay_amount()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(position.collateral_amount, Balances::collateral_deposit());
|
|
|
|
|
|
|
|
|
|
// Stablecoin total supply decreased by the burn amount.
|
|
|
|
|
let definition =
|
|
|
|
|
TokenDefinition::try_from(&state.get_account_by_id(Ids::stablecoin_definition()).data)
|
|
|
|
|
.expect("valid TokenDefinition");
|
|
|
|
|
match definition {
|
|
|
|
|
TokenDefinition::Fungible { total_supply, .. } => {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
total_supply,
|
|
|
|
|
Balances::stablecoin_supply_init() - Balances::debt_repay_amount()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
TokenDefinition::NonFungible { .. } => panic!("expected Fungible definition"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User stablecoin holding decreased by the burn amount.
|
|
|
|
|
let holding =
|
|
|
|
|
TokenHolding::try_from(&state.get_account_by_id(Ids::user_stablecoin_holding()).data)
|
|
|
|
|
.expect("valid TokenHolding");
|
|
|
|
|
match holding {
|
|
|
|
|
TokenHolding::Fungible { balance, .. } => {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
balance,
|
|
|
|
|
Balances::user_stablecoin_holding_init() - Balances::debt_repay_amount()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => {
|
|
|
|
|
panic!("expected Fungible holding")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-18 15:33:33 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn stablecoin_initialize_program_creates_globals_and_stablecoin_definition() {
|
|
|
|
|
use stablecoin_core::math::FIXED_POINT_ONE;
|
|
|
|
|
|
|
|
|
|
// `V03State::new()` no longer auto-creates the clock account; seed CLOCK_01 with this
|
|
|
|
|
// timestamp, which initialize_program reads as `now` to anchor the accumulator and
|
|
|
|
|
// redemption-price state.
|
|
|
|
|
let now: u64 = 1_700_000_000;
|
|
|
|
|
let mut state = V03State::new();
|
|
|
|
|
seed_clock(&mut state, now);
|
|
|
|
|
deploy_programs(&mut state);
|
|
|
|
|
|
|
|
|
|
// Externally-created collateral definition + market-price oracle.
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
Ids::collateral_definition(),
|
|
|
|
|
Accounts::collateral_definition_init(),
|
|
|
|
|
);
|
|
|
|
|
state.force_insert_account(
|
|
|
|
|
Ids::oracle(),
|
|
|
|
|
Accounts::oracle_init(
|
|
|
|
|
Ids::stablecoin_definition_pda(),
|
|
|
|
|
Ids::collateral_definition(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let instruction = stablecoin_core::Instruction::InitializeProgram {
|
|
|
|
|
freeze_authority_account_id: Ids::freeze_authority(),
|
|
|
|
|
initial_stability_fee_per_millisecond: FIXED_POINT_ONE + 1_500_000_000_000_000,
|
|
|
|
|
initial_controller_proportional_gain: 0,
|
|
|
|
|
initial_controller_integral_gain: 0,
|
|
|
|
|
initial_minimum_collateralization_ratio: FIXED_POINT_ONE * 3 / 2,
|
|
|
|
|
minimum_milliseconds_between_rate_updates: 300_000,
|
|
|
|
|
maximum_oracle_price_age_milliseconds: 900_000,
|
|
|
|
|
initial_redemption_price: FIXED_POINT_ONE / 2,
|
|
|
|
|
stablecoin_name: String::from("test-stable"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
|
|
|
Ids::stablecoin_program(),
|
|
|
|
|
vec![
|
|
|
|
|
Ids::admin(),
|
|
|
|
|
compute_protocol_parameters_pda(Ids::stablecoin_program()),
|
|
|
|
|
compute_stability_fee_accumulator_pda(Ids::stablecoin_program()),
|
|
|
|
|
compute_redemption_price_state_pda(Ids::stablecoin_program()),
|
|
|
|
|
Ids::stablecoin_definition_pda(),
|
|
|
|
|
compute_stablecoin_master_holding_pda(Ids::stablecoin_program()),
|
|
|
|
|
Ids::collateral_definition(),
|
|
|
|
|
Ids::oracle(),
|
|
|
|
|
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
|
|
|
|
],
|
|
|
|
|
vec![current_nonce(&state, Ids::admin())],
|
|
|
|
|
instruction,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::admin()]);
|
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
|
|
|
state
|
|
|
|
|
.transition_from_public_transaction(&tx, 1, now)
|
|
|
|
|
.expect("initialize_program must succeed");
|
|
|
|
|
|
|
|
|
|
// ProtocolParameters claimed with the expected handles.
|
|
|
|
|
let pp = stablecoin_core::ProtocolParameters::try_from(
|
|
|
|
|
&state
|
|
|
|
|
.get_account_by_id(compute_protocol_parameters_pda(Ids::stablecoin_program()))
|
|
|
|
|
.data,
|
|
|
|
|
)
|
|
|
|
|
.expect("valid ProtocolParameters");
|
|
|
|
|
assert_eq!(pp.admin_account_id, Ids::admin());
|
|
|
|
|
assert_eq!(pp.freeze_authority_account_id, Ids::freeze_authority());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
pp.stablecoin_definition_id,
|
|
|
|
|
Ids::stablecoin_definition_pda()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(pp.collateral_definition_id, Ids::collateral_definition());
|
|
|
|
|
assert_eq!(pp.market_price_oracle_id, Ids::oracle());
|
|
|
|
|
assert!(!pp.is_frozen);
|
|
|
|
|
|
|
|
|
|
// Accumulator anchored at FIXED_POINT_ONE / now.
|
|
|
|
|
let acc = stablecoin_core::StabilityFeeAccumulator::try_from(
|
|
|
|
|
&state
|
|
|
|
|
.get_account_by_id(compute_stability_fee_accumulator_pda(
|
|
|
|
|
Ids::stablecoin_program(),
|
|
|
|
|
))
|
|
|
|
|
.data,
|
|
|
|
|
)
|
|
|
|
|
.expect("valid StabilityFeeAccumulator");
|
|
|
|
|
assert_eq!(acc.accumulated_rate_at_last_accrual, FIXED_POINT_ONE);
|
|
|
|
|
assert_eq!(acc.last_accrued_at, now);
|
|
|
|
|
|
|
|
|
|
// Redemption price anchored at the initial value / now.
|
|
|
|
|
let rp = stablecoin_core::RedemptionPriceState::try_from(
|
|
|
|
|
&state
|
|
|
|
|
.get_account_by_id(compute_redemption_price_state_pda(Ids::stablecoin_program()))
|
|
|
|
|
.data,
|
|
|
|
|
)
|
|
|
|
|
.expect("valid RedemptionPriceState");
|
|
|
|
|
assert_eq!(rp.redemption_price_at_last_update, FIXED_POINT_ONE / 2);
|
|
|
|
|
assert_eq!(rp.redemption_rate_per_millisecond, FIXED_POINT_ONE);
|
|
|
|
|
assert_eq!(rp.controller_integral_term, 0);
|
|
|
|
|
assert_eq!(rp.last_updated_at, now);
|
|
|
|
|
|
|
|
|
|
// Stablecoin definition created via the chained Token::NewFungibleDefinition.
|
|
|
|
|
let definition = TokenDefinition::try_from(
|
|
|
|
|
&state
|
|
|
|
|
.get_account_by_id(Ids::stablecoin_definition_pda())
|
|
|
|
|
.data,
|
|
|
|
|
)
|
|
|
|
|
.expect("valid TokenDefinition");
|
|
|
|
|
match definition {
|
|
|
|
|
TokenDefinition::Fungible {
|
|
|
|
|
name,
|
|
|
|
|
total_supply,
|
|
|
|
|
metadata_id,
|
|
|
|
|
authority,
|
|
|
|
|
} => {
|
|
|
|
|
assert_eq!(name, "test-stable");
|
|
|
|
|
assert_eq!(total_supply, 0);
|
|
|
|
|
assert_eq!(metadata_id, None);
|
|
|
|
|
// Self/PDA authority: the definition is its own mint authority, so later
|
|
|
|
|
// debt operations can mint/burn by presenting the definition PDA seed.
|
|
|
|
|
assert_eq!(authority, Some(Ids::stablecoin_definition_pda()));
|
|
|
|
|
}
|
|
|
|
|
TokenDefinition::NonFungible { .. } => panic!("expected Fungible definition"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Empty master holding created alongside the definition.
|
|
|
|
|
let master = TokenHolding::try_from(
|
|
|
|
|
&state
|
|
|
|
|
.get_account_by_id(compute_stablecoin_master_holding_pda(
|
|
|
|
|
Ids::stablecoin_program(),
|
|
|
|
|
))
|
|
|
|
|
.data,
|
|
|
|
|
)
|
|
|
|
|
.expect("valid TokenHolding");
|
|
|
|
|
match master {
|
|
|
|
|
TokenHolding::Fungible {
|
|
|
|
|
definition_id,
|
|
|
|
|
balance,
|
|
|
|
|
} => {
|
|
|
|
|
assert_eq!(definition_id, Ids::stablecoin_definition_pda());
|
|
|
|
|
assert_eq!(balance, 0);
|
|
|
|
|
}
|
|
|
|
|
TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => {
|
|
|
|
|
panic!("expected Fungible holding")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|