use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID}; use nssa::{ program_deployment_transaction::{self, ProgramDeploymentTransaction}, public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State, }; use nssa_core::account::{Account, AccountId, Data, Nonce}; 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, }; 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") } fn user_stablecoin_holding() -> PrivateKey { PrivateKey::try_new([43; 32]).expect("valid private key") } fn admin() -> PrivateKey { PrivateKey::try_new([44; 32]).expect("valid private key") } } 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())) } fn stablecoin_definition() -> AccountId { AccountId::new([6; 32]) } fn user_stablecoin_holding() -> AccountId { AccountId::from(&PublicKey::new_from_private_key( &Keys::user_stablecoin_holding(), )) } fn position_nonce() -> u64 { 0 } fn position() -> AccountId { compute_position_pda( Self::stablecoin_program(), Self::owner(), Self::position_nonce(), ) } fn vault() -> AccountId { compute_position_vault_pda(Self::stablecoin_program(), Self::position()) } 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()) } } impl Balances { fn user_holding_init() -> u128 { 1_000_000 } fn collateral_deposit() -> u128 { 500_000 } fn collateral_withdraw() -> u128 { 200_000 } 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 } } 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, authority: None, }), 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), } } 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, authority: None, }), 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 { owner_account_id: Ids::owner(), position_nonce: Ids::position_nonce(), vault_account_id: Ids::vault(), collateral_amount: Balances::collateral_deposit(), normalized_debt_amount: Balances::initial_debt(), opened_at: 0, }), nonce: Nonce(0), } } 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); } 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 { let mut state = V03State::new(); 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 } fn state_for_stablecoin_repay_tests() -> V03State { let mut state = V03State::new(); 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 } fn assert_position(state: &V03State, expected_collateral: u128) { let position = Position::try_from(&state.get_account_by_id(Ids::position()).data).expect("valid Position"); assert_eq!(position.collateral_amount, expected_collateral); 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()); } 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 { position_nonce: Ids::position_nonce(), 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(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner()]); 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(), ); } #[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!( position.normalized_debt_amount, 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") } } } #[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") } } }