diff --git a/Cargo.lock b/Cargo.lock index 8fa1d33..9372e45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1606,6 +1606,8 @@ dependencies = [ "ata_core", "nssa", "nssa_core", + "stablecoin-methods", + "stablecoin_core", "token-methods", "token_core", ] @@ -3055,7 +3057,6 @@ dependencies = [ "nssa_core", "stablecoin_core", "token_core", - "token_program", ] [[package]] diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 5f85b31..f2374fd 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -12,6 +12,8 @@ nssa_core = { workspace = true, features = ["host"] } amm_core = { workspace = true } token_core = { workspace = true } ata_core = { workspace = true } +stablecoin_core = { workspace = true } token-methods = { path = "../token/methods" } amm-methods = { path = "../amm/methods" } ata-methods = { path = "../ata/methods" } +stablecoin-methods = { path = "../stablecoin/methods" } diff --git a/integration_tests/tests/stablecoin.rs b/integration_tests/tests/stablecoin.rs new file mode 100644 index 0000000..68c51bd --- /dev/null +++ b/integration_tests/tests/stablecoin.rs @@ -0,0 +1,237 @@ +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, 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") + } +} + +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 position() -> AccountId { + compute_position_pda( + Self::stablecoin_program(), + Self::owner(), + Self::collateral_definition(), + ) + } + + fn vault() -> AccountId { + compute_position_vault_pda(Self::stablecoin_program(), Self::position()) + } +} + +impl Balances { + fn user_holding_init() -> u128 { + 1_000_000 + } + + fn collateral_deposit() -> u128 { + 500_000 + } + + fn collateral_withdraw() -> u128 { + 200_000 + } +} + +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, + }), + 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 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_with_genesis_accounts(&[], vec![], 0); + 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 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.debt_amount, 0); + assert_eq!(position.collateral_vault_id, Ids::vault()); + assert_eq!(position.collateral_definition_id, Ids::collateral_definition()); +} + +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 { + 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(), + ); +} diff --git a/stablecoin/Cargo.toml b/stablecoin/Cargo.toml index af342fb..83a0155 100644 --- a/stablecoin/Cargo.toml +++ b/stablecoin/Cargo.toml @@ -7,6 +7,3 @@ edition = "2021" nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] } stablecoin_core = { path = "core" } token_core = { path = "../token/core" } - -[dev-dependencies] -token_program.workspace = true diff --git a/stablecoin/src/tests.rs b/stablecoin/src/tests.rs index 9b4e0ee..0bab094 100644 --- a/stablecoin/src/tests.rs +++ b/stablecoin/src/tests.rs @@ -486,30 +486,6 @@ fn withdraw_collateral_updates_position_and_emits_transfer() { assert_eq!(chained_calls[0], expected_transfer); } -#[test] -#[should_panic(expected = "Insufficient balance")] -fn withdraw_collateral_transfer_pre_states_should_not_be_executable() { - let initial_collateral: u128 = 500; - let amount: u128 = 200; - let (_post_states, chained_calls) = crate::withdraw_collateral::withdraw_collateral( - owner_account(), - init_position_account(initial_collateral, 0), - init_vault_account(), - destination_holding_account(), - STABLECOIN_PROGRAM_ID, - amount, - ); - - let transfer_call = chained_calls - .into_iter() - .next() - .expect("withdraw emits transfer"); - let [sender, recipient] = - <[_; 2]>::try_from(transfer_call.pre_states).expect("token transfer accounts"); - - token_program::transfer::transfer(sender, recipient, amount); -} - #[test] fn withdraw_collateral_allows_full_drain() { let amount: u128 = 500;