feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
#![allow(
|
|
|
|
|
clippy::indexing_slicing,
|
|
|
|
|
clippy::panic,
|
|
|
|
|
clippy::unwrap_used,
|
|
|
|
|
reason = "tests deliberately panic on bad state via assert!/#[should_panic] and index fixed-size vectors"
|
|
|
|
|
)]
|
|
|
|
|
|
|
|
|
|
use nssa_core::{
|
|
|
|
|
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
2026-06-01 11:26:15 -03:00
|
|
|
program::{AccountPostState, ChainedCall, Claim, ProgramId},
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
};
|
|
|
|
|
use stablecoin_core::{
|
|
|
|
|
compute_position_pda, compute_position_pda_seed, compute_position_vault_pda,
|
2026-06-01 11:26:15 -03:00
|
|
|
compute_position_vault_pda_seed, Position, ERR_POSITION_ACCOUNT_ID_MISMATCH,
|
|
|
|
|
ERR_POSITION_VAULT_ACCOUNT_ID_MISMATCH,
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
};
|
|
|
|
|
use token_core::{TokenDefinition, TokenHolding};
|
|
|
|
|
|
|
|
|
|
const STABLECOIN_PROGRAM_ID: ProgramId = [3u32; 8];
|
|
|
|
|
const TOKEN_PROGRAM_ID: ProgramId = [2u32; 8];
|
|
|
|
|
|
|
|
|
|
fn owner_id() -> AccountId {
|
|
|
|
|
AccountId::new([0x10u8; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collateral_definition_id() -> AccountId {
|
|
|
|
|
AccountId::new([0x20u8; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_holding_id() -> AccountId {
|
|
|
|
|
AccountId::new([0x30u8; 32])
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 10:02:12 -03:00
|
|
|
fn token_holding_account(
|
|
|
|
|
account_id: AccountId,
|
|
|
|
|
definition_id: AccountId,
|
|
|
|
|
balance: u128,
|
|
|
|
|
) -> AccountWithMetadata {
|
|
|
|
|
AccountWithMetadata {
|
|
|
|
|
account: Account {
|
|
|
|
|
program_owner: TOKEN_PROGRAM_ID,
|
|
|
|
|
balance: 0,
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
definition_id,
|
|
|
|
|
balance,
|
|
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
},
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
fn position_id() -> AccountId {
|
2026-05-12 13:04:59 -03:00
|
|
|
compute_position_pda(
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
owner_id(),
|
|
|
|
|
collateral_definition_id(),
|
|
|
|
|
)
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn vault_id() -> AccountId {
|
|
|
|
|
compute_position_vault_pda(STABLECOIN_PROGRAM_ID, position_id())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn owner_account() -> AccountWithMetadata {
|
|
|
|
|
AccountWithMetadata {
|
|
|
|
|
account: Account::default(),
|
|
|
|
|
is_authorized: true,
|
|
|
|
|
account_id: owner_id(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collateral_definition_account() -> AccountWithMetadata {
|
|
|
|
|
AccountWithMetadata {
|
|
|
|
|
account: Account {
|
|
|
|
|
program_owner: TOKEN_PROGRAM_ID,
|
|
|
|
|
balance: 0,
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
name: "SNT".to_owned(),
|
|
|
|
|
total_supply: 1_000_000,
|
|
|
|
|
metadata_id: None,
|
feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-05-27 15:04:28 +05:30
|
|
|
authority: None,
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
},
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: collateral_definition_id(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_holding_account(balance: u128) -> AccountWithMetadata {
|
2026-05-20 10:02:12 -03:00
|
|
|
let mut account = token_holding_account(user_holding_id(), collateral_definition_id(), balance);
|
|
|
|
|
account.is_authorized = true;
|
|
|
|
|
account
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn uninit_position_account() -> AccountWithMetadata {
|
|
|
|
|
AccountWithMetadata {
|
|
|
|
|
account: Account::default(),
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: position_id(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn uninit_vault_account() -> AccountWithMetadata {
|
|
|
|
|
AccountWithMetadata {
|
|
|
|
|
account: Account::default(),
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: vault_id(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-11 14:51:50 +02:00
|
|
|
|
2026-05-19 15:59:10 +02:00
|
|
|
fn destination_holding_id() -> AccountId {
|
|
|
|
|
AccountId::new([0x40u8; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn init_position_account(collateral_amount: u128, debt_amount: u128) -> AccountWithMetadata {
|
|
|
|
|
AccountWithMetadata {
|
|
|
|
|
account: Account {
|
|
|
|
|
program_owner: STABLECOIN_PROGRAM_ID,
|
|
|
|
|
balance: 0,
|
|
|
|
|
data: Data::from(&Position {
|
|
|
|
|
collateral_vault_id: vault_id(),
|
|
|
|
|
collateral_definition_id: collateral_definition_id(),
|
|
|
|
|
collateral_amount,
|
|
|
|
|
debt_amount,
|
|
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
},
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: position_id(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn init_vault_account() -> AccountWithMetadata {
|
2026-05-20 10:02:12 -03:00
|
|
|
token_holding_account(vault_id(), collateral_definition_id(), 0)
|
2026-05-19 15:59:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn destination_holding_account() -> AccountWithMetadata {
|
2026-05-20 10:02:12 -03:00
|
|
|
token_holding_account(destination_holding_id(), collateral_definition_id(), 0)
|
2026-05-19 15:59:10 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 11:09:16 +02:00
|
|
|
fn stablecoin_definition_id() -> AccountId {
|
|
|
|
|
AccountId::new([0x50u8; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_stablecoin_holding_id() -> AccountId {
|
|
|
|
|
AccountId::new([0x60u8; 32])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stablecoin_definition_account() -> AccountWithMetadata {
|
|
|
|
|
AccountWithMetadata {
|
|
|
|
|
account: Account {
|
|
|
|
|
program_owner: TOKEN_PROGRAM_ID,
|
|
|
|
|
balance: 0,
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
name: "DAI".to_owned(),
|
|
|
|
|
total_supply: 1_000_000,
|
|
|
|
|
metadata_id: None,
|
feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-05-27 15:04:28 +05:30
|
|
|
authority: None,
|
2026-05-22 11:09:16 +02:00
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
},
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: stablecoin_definition_id(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn user_stablecoin_holding_account(balance: u128) -> AccountWithMetadata {
|
|
|
|
|
let mut account = token_holding_account(
|
|
|
|
|
user_stablecoin_holding_id(),
|
|
|
|
|
stablecoin_definition_id(),
|
|
|
|
|
balance,
|
|
|
|
|
);
|
|
|
|
|
account.is_authorized = true;
|
|
|
|
|
account
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 11:26:15 -03:00
|
|
|
struct DepositCollateralFixture {
|
|
|
|
|
owner: AccountWithMetadata,
|
|
|
|
|
position: AccountWithMetadata,
|
|
|
|
|
vault: AccountWithMetadata,
|
|
|
|
|
user_holding: AccountWithMetadata,
|
|
|
|
|
token_definition: AccountWithMetadata,
|
|
|
|
|
stablecoin_program_id: ProgramId,
|
|
|
|
|
amount: u128,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DepositCollateralFixture {
|
|
|
|
|
fn run(self) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
|
|
|
|
crate::deposit_collateral::deposit_collateral(
|
|
|
|
|
self.owner,
|
|
|
|
|
self.position,
|
|
|
|
|
self.vault,
|
|
|
|
|
self.user_holding,
|
|
|
|
|
self.token_definition,
|
|
|
|
|
self.stablecoin_program_id,
|
|
|
|
|
self.amount,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn deposit_fixture() -> DepositCollateralFixture {
|
|
|
|
|
DepositCollateralFixture {
|
|
|
|
|
owner: owner_account(),
|
|
|
|
|
position: init_position_account(500, 0),
|
|
|
|
|
vault: init_vault_account(),
|
|
|
|
|
user_holding: user_holding_account(1_000),
|
|
|
|
|
token_definition: collateral_definition_account(),
|
|
|
|
|
stablecoin_program_id: STABLECOIN_PROGRAM_ID,
|
|
|
|
|
amount: 100,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_panics_with_message<F>(action: F, expected: &str)
|
|
|
|
|
where
|
|
|
|
|
F: FnOnce(),
|
|
|
|
|
{
|
|
|
|
|
let panic =
|
|
|
|
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(action)).expect_err("expected panic");
|
|
|
|
|
let message = if let Some(message) = panic.downcast_ref::<String>() {
|
|
|
|
|
message.as_str()
|
|
|
|
|
} else if let Some(message) = panic.downcast_ref::<&str>() {
|
|
|
|
|
message
|
|
|
|
|
} else {
|
|
|
|
|
panic!("panic payload must be a string");
|
|
|
|
|
};
|
|
|
|
|
assert!(
|
|
|
|
|
message.contains(expected),
|
|
|
|
|
"panic message `{message}` did not contain `{expected}`"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_deposit_collateral_panics(fixture: DepositCollateralFixture, expected: &str) {
|
|
|
|
|
assert_panics_with_message(
|
|
|
|
|
|| {
|
|
|
|
|
fixture.run();
|
|
|
|
|
},
|
|
|
|
|
expected,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn post_state_matching<'a>(
|
|
|
|
|
post_states: &'a [AccountPostState],
|
|
|
|
|
post_state_account_ids: &[AccountId],
|
|
|
|
|
expected_account_id: AccountId,
|
|
|
|
|
label: &str,
|
|
|
|
|
mut predicate: impl FnMut(&AccountPostState) -> bool,
|
|
|
|
|
) -> &'a AccountPostState {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
post_states.len(),
|
|
|
|
|
post_state_account_ids.len(),
|
|
|
|
|
"post-state account id list must match post-state length"
|
|
|
|
|
);
|
|
|
|
|
let mut matched_posts =
|
|
|
|
|
post_states
|
|
|
|
|
.iter()
|
|
|
|
|
.zip(post_state_account_ids)
|
|
|
|
|
.filter_map(|(post_state, account_id)| {
|
|
|
|
|
(*account_id == expected_account_id && predicate(post_state)).then_some(post_state)
|
|
|
|
|
});
|
|
|
|
|
let post_state = matched_posts.next().expect(label);
|
|
|
|
|
assert!(
|
|
|
|
|
matched_posts.next().is_none(),
|
|
|
|
|
"expected exactly one {label} post-state"
|
|
|
|
|
);
|
|
|
|
|
post_state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn position_post_state<'a>(
|
|
|
|
|
post_states: &'a [AccountPostState],
|
|
|
|
|
post_state_account_ids: &[AccountId],
|
|
|
|
|
expected_account_id: AccountId,
|
|
|
|
|
expected_position: &Position,
|
|
|
|
|
) -> &'a AccountPostState {
|
|
|
|
|
post_state_matching(
|
|
|
|
|
post_states,
|
|
|
|
|
post_state_account_ids,
|
|
|
|
|
expected_account_id,
|
|
|
|
|
"position post-state",
|
|
|
|
|
|post_state| {
|
|
|
|
|
post_state.account().program_owner == STABLECOIN_PROGRAM_ID
|
|
|
|
|
&& Position::try_from(&post_state.account().data)
|
|
|
|
|
.is_ok_and(|position| &position == expected_position)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
#[test]
|
|
|
|
|
fn open_position_claims_pda_and_emits_chained_calls() {
|
|
|
|
|
let collateral_amount: u128 = 500;
|
|
|
|
|
let (post_states, chained_calls) = crate::open_position::open_position(
|
|
|
|
|
owner_account(),
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
uninit_vault_account(),
|
|
|
|
|
user_holding_account(1_000),
|
|
|
|
|
collateral_definition_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
collateral_amount,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(post_states.len(), 5);
|
|
|
|
|
|
|
|
|
|
// Position is PDA-claimed and carries the encoded Position state.
|
|
|
|
|
let position_post = &post_states[1];
|
|
|
|
|
assert_eq!(
|
|
|
|
|
position_post.required_claim(),
|
2026-05-12 13:04:59 -03:00
|
|
|
Some(Claim::Pda(compute_position_pda_seed(
|
|
|
|
|
owner_id(),
|
|
|
|
|
collateral_definition_id()
|
|
|
|
|
)))
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
);
|
|
|
|
|
let position = Position::try_from(&position_post.account().data).expect("valid Position");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
position,
|
|
|
|
|
Position {
|
|
|
|
|
collateral_vault_id: vault_id(),
|
|
|
|
|
collateral_definition_id: collateral_definition_id(),
|
|
|
|
|
collateral_amount,
|
|
|
|
|
debt_amount: 0,
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-05-22 11:09:03 +02:00
|
|
|
// The runtime sets the program_owner on the claimed account after validating Claim::Pda.
|
|
|
|
|
assert_eq!(position_post.account().program_owner, ProgramId::default());
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
|
|
|
|
|
assert_eq!(chained_calls.len(), 2);
|
|
|
|
|
|
|
|
|
|
let mut vault_authorized = uninit_vault_account();
|
|
|
|
|
vault_authorized.is_authorized = true;
|
|
|
|
|
let expected_initialize = ChainedCall::new(
|
|
|
|
|
TOKEN_PROGRAM_ID,
|
|
|
|
|
vec![collateral_definition_account(), vault_authorized],
|
|
|
|
|
&token_core::Instruction::InitializeAccount,
|
|
|
|
|
)
|
|
|
|
|
.with_pda_seeds(vec![compute_position_vault_pda_seed(position_id())]);
|
|
|
|
|
assert_eq!(chained_calls[0], expected_initialize);
|
|
|
|
|
|
|
|
|
|
let post_init_vault = AccountWithMetadata {
|
|
|
|
|
account: Account {
|
|
|
|
|
program_owner: TOKEN_PROGRAM_ID,
|
|
|
|
|
balance: 0,
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
definition_id: collateral_definition_id(),
|
|
|
|
|
balance: 0,
|
|
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
},
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: vault_id(),
|
|
|
|
|
};
|
|
|
|
|
let expected_transfer = ChainedCall::new(
|
|
|
|
|
TOKEN_PROGRAM_ID,
|
|
|
|
|
vec![user_holding_account(1_000), post_init_vault],
|
|
|
|
|
&token_core::Instruction::Transfer {
|
|
|
|
|
amount_to_transfer: collateral_amount,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(chained_calls[1], expected_transfer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Owner authorization is missing")]
|
|
|
|
|
fn open_position_requires_owner_authorization() {
|
|
|
|
|
let mut owner = owner_account();
|
|
|
|
|
owner.is_authorized = false;
|
|
|
|
|
|
|
|
|
|
crate::open_position::open_position(
|
|
|
|
|
owner,
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
uninit_vault_account(),
|
|
|
|
|
user_holding_account(1_000),
|
|
|
|
|
collateral_definition_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "User collateral holding authorization is missing")]
|
|
|
|
|
fn open_position_requires_user_holding_authorization() {
|
|
|
|
|
let mut holding = user_holding_account(1_000);
|
|
|
|
|
holding.is_authorized = false;
|
|
|
|
|
|
|
|
|
|
crate::open_position::open_position(
|
|
|
|
|
owner_account(),
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
uninit_vault_account(),
|
|
|
|
|
holding,
|
|
|
|
|
collateral_definition_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position account must be uninitialized")]
|
|
|
|
|
fn open_position_rejects_initialized_position() {
|
|
|
|
|
let position = AccountWithMetadata {
|
|
|
|
|
account: Account {
|
|
|
|
|
program_owner: STABLECOIN_PROGRAM_ID,
|
|
|
|
|
balance: 0,
|
|
|
|
|
data: Data::from(&Position {
|
|
|
|
|
collateral_vault_id: vault_id(),
|
|
|
|
|
collateral_definition_id: collateral_definition_id(),
|
|
|
|
|
collateral_amount: 1,
|
|
|
|
|
debt_amount: 0,
|
|
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
},
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: position_id(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
crate::open_position::open_position(
|
|
|
|
|
owner_account(),
|
|
|
|
|
position,
|
|
|
|
|
uninit_vault_account(),
|
|
|
|
|
user_holding_account(1_000),
|
|
|
|
|
collateral_definition_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position vault account must be uninitialized")]
|
|
|
|
|
fn open_position_rejects_initialized_vault() {
|
|
|
|
|
let vault = AccountWithMetadata {
|
|
|
|
|
account: Account {
|
|
|
|
|
program_owner: TOKEN_PROGRAM_ID,
|
|
|
|
|
balance: 0,
|
|
|
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
|
|
|
definition_id: collateral_definition_id(),
|
|
|
|
|
balance: 0,
|
|
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
},
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: vault_id(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
crate::open_position::open_position(
|
|
|
|
|
owner_account(),
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
vault,
|
|
|
|
|
user_holding_account(1_000),
|
|
|
|
|
collateral_definition_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position account ID does not match expected derivation")]
|
|
|
|
|
fn open_position_rejects_wrong_position_address() {
|
|
|
|
|
let bad_position = AccountWithMetadata {
|
|
|
|
|
account: Account::default(),
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: AccountId::new([0xFFu8; 32]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
crate::open_position::open_position(
|
|
|
|
|
owner_account(),
|
|
|
|
|
bad_position,
|
|
|
|
|
uninit_vault_account(),
|
|
|
|
|
user_holding_account(1_000),
|
|
|
|
|
collateral_definition_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position vault account ID does not match expected derivation")]
|
|
|
|
|
fn open_position_rejects_wrong_vault_address() {
|
|
|
|
|
let bad_vault = AccountWithMetadata {
|
|
|
|
|
account: Account::default(),
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: AccountId::new([0xEEu8; 32]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
crate::open_position::open_position(
|
|
|
|
|
owner_account(),
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
bad_vault,
|
|
|
|
|
user_holding_account(1_000),
|
|
|
|
|
collateral_definition_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "User collateral holding does not match the provided token definition")]
|
|
|
|
|
fn open_position_rejects_mismatched_token_definition() {
|
|
|
|
|
let other_definition = AccountWithMetadata {
|
|
|
|
|
account: Account {
|
|
|
|
|
program_owner: TOKEN_PROGRAM_ID,
|
|
|
|
|
balance: 0,
|
|
|
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
|
|
|
name: "OTHER".to_owned(),
|
|
|
|
|
total_supply: 1,
|
|
|
|
|
metadata_id: None,
|
feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-05-27 15:04:28 +05:30
|
|
|
authority: None,
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
},
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: AccountId::new([0x21u8; 32]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
crate::open_position::open_position(
|
|
|
|
|
owner_account(),
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
uninit_vault_account(),
|
|
|
|
|
user_holding_account(1_000),
|
|
|
|
|
other_definition,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(
|
|
|
|
|
expected = "Collateral token definition is not owned by the user holding's Token Program"
|
|
|
|
|
)]
|
|
|
|
|
fn open_position_rejects_definition_with_wrong_token_program() {
|
|
|
|
|
let mut definition = collateral_definition_account();
|
|
|
|
|
definition.account.program_owner = [9u32; 8];
|
|
|
|
|
|
|
|
|
|
crate::open_position::open_position(
|
|
|
|
|
owner_account(),
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
uninit_vault_account(),
|
|
|
|
|
user_holding_account(1_000),
|
|
|
|
|
definition,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-05-12 13:04:59 -03:00
|
|
|
fn position_pda_is_deterministic_and_owner_and_collateral_specific() {
|
|
|
|
|
let id_a = compute_position_pda(
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
owner_id(),
|
|
|
|
|
collateral_definition_id(),
|
|
|
|
|
);
|
|
|
|
|
let id_b = compute_position_pda(
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
owner_id(),
|
|
|
|
|
collateral_definition_id(),
|
|
|
|
|
);
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
assert_eq!(id_a, id_b);
|
|
|
|
|
|
|
|
|
|
let other_owner = AccountId::new([0x11u8; 32]);
|
|
|
|
|
assert_ne!(
|
2026-05-12 13:04:59 -03:00
|
|
|
compute_position_pda(
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
other_owner,
|
|
|
|
|
collateral_definition_id()
|
|
|
|
|
),
|
|
|
|
|
id_a
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let other_definition = AccountId::new([0x21u8; 32]);
|
|
|
|
|
assert_ne!(
|
|
|
|
|
compute_position_pda(STABLECOIN_PROGRAM_ID, owner_id(), other_definition),
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
id_a
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn position_pda_and_vault_pda_do_not_collide() {
|
2026-05-12 13:04:59 -03:00
|
|
|
// Distinct domain tags must keep the position id and its vault id disjoint.
|
|
|
|
|
let position = compute_position_pda(
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
owner_id(),
|
|
|
|
|
collateral_definition_id(),
|
|
|
|
|
);
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
let vault = compute_position_vault_pda(STABLECOIN_PROGRAM_ID, position);
|
|
|
|
|
assert_ne!(position, vault);
|
|
|
|
|
}
|
2026-05-19 15:59:10 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn withdraw_collateral_updates_position_and_emits_transfer() {
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(post_states.len(), 4);
|
|
|
|
|
|
|
|
|
|
// Position post-state: plain `new`, holds the decremented Position.
|
|
|
|
|
let position_post = &post_states[1];
|
|
|
|
|
assert_eq!(position_post.required_claim(), None);
|
|
|
|
|
let position = Position::try_from(&position_post.account().data).expect("valid Position");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
position,
|
|
|
|
|
Position {
|
|
|
|
|
collateral_vault_id: vault_id(),
|
|
|
|
|
collateral_definition_id: collateral_definition_id(),
|
|
|
|
|
collateral_amount: initial_collateral - amount,
|
|
|
|
|
debt_amount: 0,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(position_post.account().program_owner, STABLECOIN_PROGRAM_ID);
|
|
|
|
|
|
|
|
|
|
// Vault and destination post-states are pre-transfer (mutation comes via chained call).
|
|
|
|
|
assert_eq!(post_states[2].account(), &init_vault_account().account);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
post_states[3].account(),
|
|
|
|
|
&destination_holding_account().account
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Single chained Token::Transfer with vault PDA seed.
|
|
|
|
|
assert_eq!(chained_calls.len(), 1);
|
|
|
|
|
let mut vault_authorized = init_vault_account();
|
|
|
|
|
vault_authorized.is_authorized = true;
|
|
|
|
|
let expected_transfer = ChainedCall::new(
|
|
|
|
|
TOKEN_PROGRAM_ID,
|
|
|
|
|
vec![vault_authorized, destination_holding_account()],
|
|
|
|
|
&token_core::Instruction::Transfer {
|
|
|
|
|
amount_to_transfer: amount,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.with_pda_seeds(vec![compute_position_vault_pda_seed(position_id())]);
|
|
|
|
|
assert_eq!(chained_calls[0], expected_transfer);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 10:05:49 -03:00
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_updates_position_and_emits_transfer() {
|
|
|
|
|
let initial_collateral: u128 = 500;
|
|
|
|
|
let initial_debt: u128 = 300;
|
|
|
|
|
let amount: u128 = 200;
|
|
|
|
|
let holding_balance: u128 = 1_000;
|
2026-06-01 11:26:15 -03:00
|
|
|
let position_account = init_position_account(initial_collateral, initial_debt);
|
|
|
|
|
let vault = init_vault_account();
|
|
|
|
|
let user_holding = user_holding_account(holding_balance);
|
2026-05-27 10:05:49 -03:00
|
|
|
|
|
|
|
|
let (post_states, chained_calls) = crate::deposit_collateral::deposit_collateral(
|
|
|
|
|
owner_account(),
|
2026-06-01 11:26:15 -03:00
|
|
|
position_account.clone(),
|
|
|
|
|
vault.clone(),
|
|
|
|
|
user_holding.clone(),
|
|
|
|
|
collateral_definition_account(),
|
2026-05-27 10:05:49 -03:00
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
amount,
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-01 11:26:15 -03:00
|
|
|
assert_eq!(post_states.len(), 2);
|
|
|
|
|
assert!(post_states
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|post_state| post_state.account().program_owner != TOKEN_PROGRAM_ID));
|
2026-05-27 10:05:49 -03:00
|
|
|
|
2026-06-01 11:26:15 -03:00
|
|
|
let expected_position = Position {
|
|
|
|
|
collateral_vault_id: vault_id(),
|
|
|
|
|
collateral_definition_id: collateral_definition_id(),
|
|
|
|
|
collateral_amount: initial_collateral + amount,
|
|
|
|
|
debt_amount: initial_debt,
|
|
|
|
|
};
|
|
|
|
|
let post_state_account_ids = [owner_id(), position_account.account_id];
|
|
|
|
|
let position_post = position_post_state(
|
|
|
|
|
&post_states,
|
|
|
|
|
&post_state_account_ids,
|
|
|
|
|
position_account.account_id,
|
|
|
|
|
&expected_position,
|
|
|
|
|
);
|
2026-05-27 10:05:49 -03:00
|
|
|
assert_eq!(position_post.required_claim(), None);
|
|
|
|
|
let position = Position::try_from(&position_post.account().data).expect("valid Position");
|
2026-06-01 11:26:15 -03:00
|
|
|
assert_eq!(position, expected_position);
|
2026-05-27 10:05:49 -03:00
|
|
|
assert_eq!(position_post.account().program_owner, STABLECOIN_PROGRAM_ID);
|
|
|
|
|
|
|
|
|
|
assert_eq!(chained_calls.len(), 1);
|
|
|
|
|
let expected_transfer = ChainedCall::new(
|
|
|
|
|
TOKEN_PROGRAM_ID,
|
2026-06-01 11:26:15 -03:00
|
|
|
vec![user_holding, vault],
|
2026-05-27 10:05:49 -03:00
|
|
|
&token_core::Instruction::Transfer {
|
|
|
|
|
amount_to_transfer: amount,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(chained_calls[0], expected_transfer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_allows_zero_amount() {
|
|
|
|
|
let initial: u128 = 500;
|
2026-06-01 11:26:15 -03:00
|
|
|
let position_account = init_position_account(initial, 0);
|
2026-05-27 10:05:49 -03:00
|
|
|
let (post_states, chained_calls) = crate::deposit_collateral::deposit_collateral(
|
|
|
|
|
owner_account(),
|
2026-06-01 11:26:15 -03:00
|
|
|
position_account.clone(),
|
2026-05-27 10:05:49 -03:00
|
|
|
init_vault_account(),
|
|
|
|
|
user_holding_account(1_000),
|
2026-06-01 11:26:15 -03:00
|
|
|
collateral_definition_account(),
|
2026-05-27 10:05:49 -03:00
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
0,
|
|
|
|
|
);
|
2026-06-01 11:26:15 -03:00
|
|
|
let expected_position = Position {
|
|
|
|
|
collateral_vault_id: vault_id(),
|
|
|
|
|
collateral_definition_id: collateral_definition_id(),
|
|
|
|
|
collateral_amount: initial,
|
|
|
|
|
debt_amount: 0,
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(post_states.len(), 2);
|
|
|
|
|
assert!(post_states
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|post_state| post_state.account().program_owner != TOKEN_PROGRAM_ID));
|
|
|
|
|
let post_state_account_ids = [owner_id(), position_account.account_id];
|
|
|
|
|
let position_post = position_post_state(
|
|
|
|
|
&post_states,
|
|
|
|
|
&post_state_account_ids,
|
|
|
|
|
position_account.account_id,
|
|
|
|
|
&expected_position,
|
|
|
|
|
);
|
|
|
|
|
let position = Position::try_from(&position_post.account().data).expect("valid Position");
|
2026-05-27 10:05:49 -03:00
|
|
|
assert_eq!(position.collateral_amount, initial);
|
2026-06-01 11:26:15 -03:00
|
|
|
assert!(chained_calls.is_empty());
|
|
|
|
|
}
|
2026-05-27 10:05:49 -03:00
|
|
|
|
2026-06-01 11:26:15 -03:00
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_zero_amount_validates_token_definition() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.amount = 0;
|
|
|
|
|
fixture.token_definition.account.data = Data::try_from(vec![0xFC]).expect("test data fits");
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_DEFINITION_INVALID,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-06-01 11:26:15 -03:00
|
|
|
fn deposit_collateral_zero_amount_validates_vault_owner() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.amount = 0;
|
|
|
|
|
fixture.vault.account.program_owner = [9u32; 8];
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_PROGRAM_MISMATCH,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_zero_amount_validates_user_holding_data() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.amount = 0;
|
|
|
|
|
fixture.user_holding.account.data = Data::try_from(vec![0xFD]).expect("test data fits");
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(fixture, crate::deposit_collateral::ERR_USER_HOLDING_INVALID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-05-27 10:05:49 -03:00
|
|
|
fn deposit_collateral_requires_owner_authorization() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.owner.is_authorized = false;
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_OWNER_AUTHORIZATION_MISSING,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_requires_user_holding_authorization() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.user_holding.is_authorized = false;
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_USER_HOLDING_AUTHORIZATION_MISSING,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_uninitialized_position() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.position = uninit_position_account();
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_POSITION_UNINITIALIZED,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_default_owned_position_as_uninitialized() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.position.account.program_owner = ProgramId::default();
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_POSITION_UNINITIALIZED,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_position_owned_by_other_program() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.position.account.program_owner = [9u32; 8];
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_POSITION_WRONG_PROGRAM_OWNER,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-06-01 11:26:15 -03:00
|
|
|
fn deposit_collateral_rejects_invalid_position_data() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.position.account.data = Data::try_from(vec![0xFB]).expect("test data fits");
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_POSITION_INVALID_STATE,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-06-01 11:26:15 -03:00
|
|
|
fn deposit_collateral_rejects_wrong_position_address() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.position.account_id = AccountId::new([0xFFu8; 32]);
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(fixture, ERR_POSITION_ACCOUNT_ID_MISMATCH);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-05-27 10:05:49 -03:00
|
|
|
fn deposit_collateral_rejects_wrong_vault_address() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.vault.account_id = AccountId::new([0xEEu8; 32]);
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(fixture, ERR_POSITION_VAULT_ACCOUNT_ID_MISMATCH);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_position_vault_id_mismatch() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.position.account.data = Data::from(&Position {
|
|
|
|
|
collateral_vault_id: AccountId::new([0x71u8; 32]),
|
|
|
|
|
collateral_definition_id: collateral_definition_id(),
|
|
|
|
|
collateral_amount: 500,
|
|
|
|
|
debt_amount: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_POSITION_VAULT_MISMATCH,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_uninitialized_vault() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.vault = uninit_vault_account();
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(fixture, crate::deposit_collateral::ERR_VAULT_UNINITIALIZED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_default_owned_vault_as_uninitialized() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.vault.account.program_owner = ProgramId::default();
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(fixture, crate::deposit_collateral::ERR_VAULT_UNINITIALIZED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_invalid_vault_holding_data() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.vault.account.data = Data::try_from(vec![0xFA]).expect("test data fits");
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_VAULT_INVALID_HOLDING,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_vault_for_other_definition() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.vault.account.data = Data::from(&TokenHolding::Fungible {
|
2026-05-27 10:05:49 -03:00
|
|
|
definition_id: AccountId::new([0x21u8; 32]),
|
|
|
|
|
balance: 0,
|
|
|
|
|
});
|
2026-06-01 11:26:15 -03:00
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_VAULT_WRONG_DEFINITION,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_nonfungible_vault() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.vault.account.data = Data::from(&TokenHolding::NftPrintedCopy {
|
|
|
|
|
definition_id: collateral_definition_id(),
|
|
|
|
|
owned: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(fixture, crate::deposit_collateral::ERR_VAULT_NOT_FUNGIBLE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_vault_definition_owner_mismatch() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.vault.account.program_owner = [9u32; 8];
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_PROGRAM_MISMATCH,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_uninitialized_user_holding() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.user_holding = AccountWithMetadata {
|
2026-05-27 10:05:49 -03:00
|
|
|
account: Account::default(),
|
|
|
|
|
is_authorized: true,
|
|
|
|
|
account_id: user_holding_id(),
|
|
|
|
|
};
|
2026-06-01 11:26:15 -03:00
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_USER_HOLDING_UNINITIALIZED,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_default_owned_user_holding_as_uninitialized() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.user_holding.account.program_owner = ProgramId::default();
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_USER_HOLDING_UNINITIALIZED,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_holding_with_different_token_program() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.user_holding.account.program_owner = [9u32; 8];
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_PROGRAM_MISMATCH,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_holding_for_other_definition() {
|
2026-06-01 11:26:15 -03:00
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.user_holding.account.data = Data::from(&TokenHolding::Fungible {
|
2026-05-27 10:05:49 -03:00
|
|
|
definition_id: AccountId::new([0x21u8; 32]),
|
|
|
|
|
balance: 1_000,
|
|
|
|
|
});
|
2026-06-01 11:26:15 -03:00
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_USER_HOLDING_WRONG_DEFINITION,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-06-01 11:26:15 -03:00
|
|
|
fn deposit_collateral_rejects_insufficient_user_balance() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.user_holding = user_holding_account(99);
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_USER_HOLDING_INSUFFICIENT_BALANCE,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_nonfungible_user_holding() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.amount = 1;
|
|
|
|
|
fixture.user_holding.account.data = Data::from(&TokenHolding::NftPrintedCopy {
|
|
|
|
|
definition_id: collateral_definition_id(),
|
|
|
|
|
owned: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_USER_HOLDING_NOT_FUNGIBLE,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_other_token_definition() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.token_definition.account_id = AccountId::new([0x21u8; 32]);
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_DEFINITION_MISMATCH,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_token_definition_with_wrong_token_program() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.token_definition.account.program_owner = [9u32; 8];
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_PROGRAM_MISMATCH,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_uninitialized_token_definition() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.token_definition.account = Account::default();
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_DEFINITION_UNINITIALIZED,
|
2026-05-27 10:05:49 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 11:26:15 -03:00
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_invalid_token_definition_data() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.token_definition.account.data = Data::try_from(vec![0xFF]).expect("test data fits");
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_DEFINITION_INVALID,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_nonfungible_token_definition() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.token_definition.account.data = Data::from(&TokenDefinition::NonFungible {
|
|
|
|
|
name: "NFT".to_owned(),
|
|
|
|
|
printable_supply: 1,
|
|
|
|
|
metadata_id: AccountId::new([0x70u8; 32]),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(
|
|
|
|
|
fixture,
|
|
|
|
|
crate::deposit_collateral::ERR_TOKEN_DEFINITION_NOT_FUNGIBLE,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deposit_collateral_rejects_collateral_overflow() {
|
|
|
|
|
let mut fixture = deposit_fixture();
|
|
|
|
|
fixture.position = init_position_account(u128::MAX, 0);
|
|
|
|
|
fixture.amount = 1;
|
|
|
|
|
|
|
|
|
|
assert_deposit_collateral_panics(fixture, crate::deposit_collateral::ERR_COLLATERAL_OVERFLOW);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:59:10 +02:00
|
|
|
#[test]
|
|
|
|
|
fn withdraw_collateral_allows_full_drain() {
|
|
|
|
|
let amount: u128 = 500;
|
|
|
|
|
let (post_states, _chained_calls) = crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(amount, 0),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
amount,
|
|
|
|
|
);
|
|
|
|
|
let position = Position::try_from(&post_states[1].account().data).expect("valid Position");
|
|
|
|
|
assert_eq!(position.collateral_amount, 0);
|
|
|
|
|
assert_eq!(position.debt_amount, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn withdraw_collateral_allows_zero_amount() {
|
|
|
|
|
let initial: u128 = 500;
|
|
|
|
|
let (post_states, chained_calls) = crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(initial, 0),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
let position = Position::try_from(&post_states[1].account().data).expect("valid Position");
|
|
|
|
|
assert_eq!(position.collateral_amount, initial);
|
|
|
|
|
|
|
|
|
|
let mut vault_authorized = init_vault_account();
|
|
|
|
|
vault_authorized.is_authorized = true;
|
|
|
|
|
let expected_transfer = ChainedCall::new(
|
|
|
|
|
TOKEN_PROGRAM_ID,
|
|
|
|
|
vec![vault_authorized, destination_holding_account()],
|
|
|
|
|
&token_core::Instruction::Transfer {
|
|
|
|
|
amount_to_transfer: 0,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.with_pda_seeds(vec![compute_position_vault_pda_seed(position_id())]);
|
|
|
|
|
assert_eq!(chained_calls, vec![expected_transfer]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Owner authorization is missing")]
|
|
|
|
|
fn withdraw_collateral_requires_owner_authorization() {
|
|
|
|
|
let mut owner = owner_account();
|
|
|
|
|
owner.is_authorized = false;
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner,
|
|
|
|
|
init_position_account(500, 0),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position account must be initialized")]
|
|
|
|
|
fn withdraw_collateral_rejects_uninitialized_position() {
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position is not owned by this stablecoin program")]
|
|
|
|
|
fn withdraw_collateral_rejects_position_owned_by_other_program() {
|
|
|
|
|
let mut position = init_position_account(500, 0);
|
|
|
|
|
position.account.program_owner = [9u32; 8];
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
position,
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position account ID does not match expected derivation")]
|
|
|
|
|
fn withdraw_collateral_rejects_wrong_position_address() {
|
|
|
|
|
let mut position = init_position_account(500, 0);
|
|
|
|
|
position.account_id = AccountId::new([0xFFu8; 32]);
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
position,
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position vault account ID does not match expected derivation")]
|
|
|
|
|
fn withdraw_collateral_rejects_wrong_vault_address() {
|
|
|
|
|
let mut vault = init_vault_account();
|
|
|
|
|
vault.account_id = AccountId::new([0xEEu8; 32]);
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 0),
|
|
|
|
|
vault,
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Vault token holding is not for the position's collateral definition")]
|
|
|
|
|
fn withdraw_collateral_rejects_vault_for_other_definition() {
|
|
|
|
|
let mut vault = init_vault_account();
|
|
|
|
|
vault.account.data = Data::from(&TokenHolding::Fungible {
|
|
|
|
|
definition_id: AccountId::new([0x21u8; 32]),
|
|
|
|
|
balance: 0,
|
|
|
|
|
});
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 0),
|
|
|
|
|
vault,
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Destination must be initialized")]
|
|
|
|
|
fn withdraw_collateral_rejects_uninitialized_destination() {
|
|
|
|
|
let destination = AccountWithMetadata {
|
|
|
|
|
account: Account::default(),
|
|
|
|
|
is_authorized: false,
|
|
|
|
|
account_id: destination_holding_id(),
|
|
|
|
|
};
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 0),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Destination must be owned by the same Token Program as the vault")]
|
|
|
|
|
fn withdraw_collateral_rejects_destination_with_wrong_token_program() {
|
|
|
|
|
let mut destination = destination_holding_account();
|
|
|
|
|
destination.account.program_owner = [9u32; 8];
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 0),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(
|
|
|
|
|
expected = "Destination token definition does not match the position's collateral definition"
|
|
|
|
|
)]
|
|
|
|
|
fn withdraw_collateral_rejects_destination_for_other_definition() {
|
|
|
|
|
let mut destination = destination_holding_account();
|
|
|
|
|
destination.account.data = Data::from(&TokenHolding::Fungible {
|
|
|
|
|
definition_id: AccountId::new([0x21u8; 32]),
|
|
|
|
|
balance: 0,
|
|
|
|
|
});
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 0),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "withdraw_collateral with debt is not supported yet")]
|
|
|
|
|
fn withdraw_collateral_rejects_withdrawal_with_outstanding_debt() {
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 1),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Withdrawal amount exceeds position collateral")]
|
|
|
|
|
fn withdraw_collateral_rejects_overdraw() {
|
|
|
|
|
crate::withdraw_collateral::withdraw_collateral(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(100, 0),
|
|
|
|
|
init_vault_account(),
|
|
|
|
|
destination_holding_account(),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
200,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-22 11:09:16 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn repay_debt_decreases_debt_and_emits_burn() {
|
|
|
|
|
let initial_collateral: u128 = 500;
|
|
|
|
|
let initial_debt: u128 = 300;
|
|
|
|
|
let amount: u128 = 100;
|
|
|
|
|
let holding_balance: u128 = 1_000;
|
|
|
|
|
|
|
|
|
|
let (post_states, chained_calls) = crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(initial_collateral, initial_debt),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(holding_balance),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
amount,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(post_states.len(), 4);
|
|
|
|
|
|
|
|
|
|
// Position post-state: plain `new`, holds the decremented Position.
|
|
|
|
|
let position_post = &post_states[1];
|
|
|
|
|
assert_eq!(position_post.required_claim(), None);
|
|
|
|
|
let position = Position::try_from(&position_post.account().data).expect("valid Position");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
position,
|
|
|
|
|
Position {
|
|
|
|
|
collateral_vault_id: vault_id(),
|
|
|
|
|
collateral_definition_id: collateral_definition_id(),
|
|
|
|
|
collateral_amount: initial_collateral,
|
|
|
|
|
debt_amount: initial_debt - amount,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(position_post.account().program_owner, STABLECOIN_PROGRAM_ID);
|
|
|
|
|
|
|
|
|
|
// Stablecoin definition and user holding post-states are pre-burn.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
post_states[2].account(),
|
|
|
|
|
&stablecoin_definition_account().account
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
post_states[3].account(),
|
|
|
|
|
&user_stablecoin_holding_account(holding_balance).account
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Single chained Token::Burn, no PDA seeds (user-authorized burn source).
|
|
|
|
|
assert_eq!(chained_calls.len(), 1);
|
|
|
|
|
let expected_burn = ChainedCall::new(
|
|
|
|
|
TOKEN_PROGRAM_ID,
|
|
|
|
|
vec![
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(holding_balance),
|
|
|
|
|
],
|
|
|
|
|
&token_core::Instruction::Burn {
|
|
|
|
|
amount_to_burn: amount,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(chained_calls[0], expected_burn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn repay_debt_allows_full_repayment() {
|
|
|
|
|
let debt: u128 = 300;
|
|
|
|
|
let (post_states, _chained_calls) = crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, debt),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(1_000),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
debt,
|
|
|
|
|
);
|
|
|
|
|
let position = Position::try_from(&post_states[1].account().data).expect("valid Position");
|
|
|
|
|
assert_eq!(position.debt_amount, 0);
|
|
|
|
|
assert_eq!(position.collateral_amount, 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn repay_debt_allows_zero_amount() {
|
|
|
|
|
let initial_debt: u128 = 300;
|
|
|
|
|
let (post_states, chained_calls) = crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, initial_debt),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(1_000),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
let position = Position::try_from(&post_states[1].account().data).expect("valid Position");
|
|
|
|
|
assert_eq!(position.debt_amount, initial_debt);
|
|
|
|
|
|
|
|
|
|
let expected_burn = ChainedCall::new(
|
|
|
|
|
TOKEN_PROGRAM_ID,
|
|
|
|
|
vec![
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(1_000),
|
|
|
|
|
],
|
|
|
|
|
&token_core::Instruction::Burn { amount_to_burn: 0 },
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(chained_calls, vec![expected_burn]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Owner authorization is missing")]
|
|
|
|
|
fn repay_debt_requires_owner_authorization() {
|
|
|
|
|
let mut owner = owner_account();
|
|
|
|
|
owner.is_authorized = false;
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner,
|
|
|
|
|
init_position_account(500, 300),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(1_000),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position account must be initialized")]
|
|
|
|
|
fn repay_debt_rejects_uninitialized_position() {
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
uninit_position_account(),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(1_000),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position is not owned by this stablecoin program")]
|
|
|
|
|
fn repay_debt_rejects_position_owned_by_other_program() {
|
|
|
|
|
let mut position = init_position_account(500, 300);
|
|
|
|
|
position.account.program_owner = [9u32; 8];
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
position,
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(1_000),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Position account ID does not match expected derivation")]
|
|
|
|
|
fn repay_debt_rejects_wrong_position_address() {
|
|
|
|
|
let mut position = init_position_account(500, 300);
|
|
|
|
|
position.account_id = AccountId::new([0xFFu8; 32]);
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
position,
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(1_000),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "User stablecoin holding authorization is missing")]
|
|
|
|
|
fn repay_debt_requires_user_holding_authorization() {
|
|
|
|
|
let mut holding = user_stablecoin_holding_account(1_000);
|
|
|
|
|
holding.is_authorized = false;
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 300),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
holding,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "User stablecoin holding must be initialized")]
|
|
|
|
|
fn repay_debt_rejects_uninitialized_user_holding() {
|
|
|
|
|
let holding = AccountWithMetadata {
|
|
|
|
|
account: Account::default(),
|
|
|
|
|
is_authorized: true,
|
|
|
|
|
account_id: user_stablecoin_holding_id(),
|
|
|
|
|
};
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 300),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
holding,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(
|
|
|
|
|
expected = "Stablecoin holding and definition must be owned by the same Token Program"
|
|
|
|
|
)]
|
|
|
|
|
fn repay_debt_rejects_holding_with_different_token_program() {
|
|
|
|
|
let mut holding = user_stablecoin_holding_account(1_000);
|
|
|
|
|
holding.account.program_owner = [9u32; 8];
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 300),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
holding,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Stablecoin holding does not match the provided stablecoin definition")]
|
|
|
|
|
fn repay_debt_rejects_holding_for_other_definition() {
|
|
|
|
|
let mut holding = user_stablecoin_holding_account(1_000);
|
|
|
|
|
holding.account.data = Data::from(&TokenHolding::Fungible {
|
|
|
|
|
definition_id: AccountId::new([0x21u8; 32]),
|
|
|
|
|
balance: 1_000,
|
|
|
|
|
});
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 300),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
holding,
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
100,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic(expected = "Repay amount exceeds outstanding debt")]
|
|
|
|
|
fn repay_debt_rejects_overrepay() {
|
|
|
|
|
crate::repay_debt::repay_debt(
|
|
|
|
|
owner_account(),
|
|
|
|
|
init_position_account(500, 100),
|
|
|
|
|
stablecoin_definition_account(),
|
|
|
|
|
user_stablecoin_holding_account(1_000),
|
|
|
|
|
STABLECOIN_PROGRAM_ID,
|
|
|
|
|
200,
|
|
|
|
|
);
|
|
|
|
|
}
|