mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-03 05:29:50 +00:00
fix(stablecoin): harden collateral deposits
This commit is contained in:
parent
f111c55b09
commit
056b7a5102
@ -49,23 +49,29 @@
|
||||
{
|
||||
"name": "owner",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "position",
|
||||
"writable": false,
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"writable": false,
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_holding",
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "token_definition",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
|
||||
@ -304,6 +304,7 @@ fn stablecoin_open_position_deposit_then_withdraw_collateral() {
|
||||
Ids::position(),
|
||||
Ids::vault(),
|
||||
Ids::user_holding(),
|
||||
Ids::collateral_definition(),
|
||||
],
|
||||
vec![
|
||||
current_nonce(&state, Ids::owner()),
|
||||
|
||||
@ -15,6 +15,11 @@ use spel_framework_macros::account_type;
|
||||
const POSITION_PDA_DOMAIN: &[u8] = b"POSITION";
|
||||
const POSITION_VAULT_PDA_DOMAIN: &[u8] = b"POSITION_VAULT";
|
||||
|
||||
pub const ERR_POSITION_ACCOUNT_ID_MISMATCH: &str =
|
||||
"Position account ID does not match expected derivation";
|
||||
pub const ERR_POSITION_VAULT_ACCOUNT_ID_MISMATCH: &str =
|
||||
"Position vault account ID does not match expected derivation";
|
||||
|
||||
/// Stablecoin Program Instruction.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
@ -36,14 +41,17 @@ pub enum Instruction {
|
||||
},
|
||||
/// Deposit additional collateral tokens into an existing position vault.
|
||||
///
|
||||
/// Required accounts (4):
|
||||
/// Required accounts (5):
|
||||
/// - Owner account (authorized; binds caller-as-owner via position PDA re-derivation)
|
||||
/// - Position account (initialized, owned by `self_program_id`)
|
||||
/// - Position vault token holding (address must match
|
||||
/// `compute_position_vault_pda(self_program_id, position_id)`)
|
||||
/// - User's source token holding for the collateral (authorized, initialized, owned by the
|
||||
/// same Token Program as the vault, with `TokenHolding.definition_id ==
|
||||
/// same Token Program as the token definition, with `TokenHolding.definition_id ==
|
||||
/// Position.collateral_definition_id`)
|
||||
/// - Token definition account for the collateral (matches `Position.collateral_definition_id`;
|
||||
/// must be fungible, and its `program_owner` determines the Token Program used by the
|
||||
/// chained `Transfer` call)
|
||||
///
|
||||
/// No collateralization check is needed because this instruction never increases debt.
|
||||
DepositCollateral {
|
||||
@ -206,10 +214,12 @@ pub fn verify_position_and_get_seed(
|
||||
) -> PdaSeed {
|
||||
let seed = compute_position_pda_seed(owner.account_id, collateral_definition_id);
|
||||
let expected_id = AccountId::for_public_pda(&stablecoin_program_id, &seed);
|
||||
assert_eq!(
|
||||
position.account_id, expected_id,
|
||||
"Position account ID does not match expected derivation"
|
||||
);
|
||||
if position.account_id != expected_id {
|
||||
panic!(
|
||||
"{ERR_POSITION_ACCOUNT_ID_MISMATCH}: provided {}, expected {}",
|
||||
position.account_id, expected_id
|
||||
);
|
||||
}
|
||||
seed
|
||||
}
|
||||
|
||||
@ -226,9 +236,11 @@ pub fn verify_position_vault_and_get_seed(
|
||||
) -> PdaSeed {
|
||||
let seed = compute_position_vault_pda_seed(position_id);
|
||||
let expected_id = AccountId::for_public_pda(&stablecoin_program_id, &seed);
|
||||
assert_eq!(
|
||||
vault.account_id, expected_id,
|
||||
"Position vault account ID does not match expected derivation"
|
||||
);
|
||||
if vault.account_id != expected_id {
|
||||
panic!(
|
||||
"{ERR_POSITION_VAULT_ACCOUNT_ID_MISMATCH}: provided {}, expected {}",
|
||||
vault.account_id, expected_id
|
||||
);
|
||||
}
|
||||
seed
|
||||
}
|
||||
|
||||
@ -56,10 +56,15 @@ mod stablecoin {
|
||||
#[instruction]
|
||||
pub fn deposit_collateral(
|
||||
ctx: ProgramContext,
|
||||
#[account(signer)]
|
||||
owner: AccountWithMetadata,
|
||||
#[account(mut)]
|
||||
position: AccountWithMetadata,
|
||||
#[account(mut)]
|
||||
vault: AccountWithMetadata,
|
||||
#[account(mut, signer)]
|
||||
user_holding: AccountWithMetadata,
|
||||
token_definition: AccountWithMetadata,
|
||||
amount: u128,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) =
|
||||
@ -68,6 +73,7 @@ mod stablecoin {
|
||||
position,
|
||||
vault,
|
||||
user_holding,
|
||||
token_definition,
|
||||
ctx.self_program_id,
|
||||
amount,
|
||||
);
|
||||
|
||||
@ -1,15 +1,61 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
program::{AccountPostState, ChainedCall, ProgramId, DEFAULT_PROGRAM_ID},
|
||||
};
|
||||
use stablecoin_core::{verify_position_and_get_seed, verify_position_vault_and_get_seed, Position};
|
||||
use token_core::TokenHolding;
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
|
||||
pub(crate) const ERR_OWNER_AUTHORIZATION_MISSING: &str = "Owner authorization is missing";
|
||||
pub(crate) const ERR_USER_HOLDING_AUTHORIZATION_MISSING: &str =
|
||||
"User collateral holding authorization is missing";
|
||||
pub(crate) const ERR_POSITION_UNINITIALIZED: &str = "Position account must be initialized";
|
||||
pub(crate) const ERR_POSITION_WRONG_PROGRAM_OWNER: &str =
|
||||
"Position is not owned by this stablecoin program";
|
||||
pub(crate) const ERR_VAULT_UNINITIALIZED: &str = "Vault must be initialized";
|
||||
pub(crate) const ERR_USER_HOLDING_UNINITIALIZED: &str =
|
||||
"User collateral holding must be initialized";
|
||||
pub(crate) const ERR_POSITION_INVALID_STATE: &str =
|
||||
"Position account must hold valid Position state";
|
||||
pub(crate) const ERR_POSITION_VAULT_MISMATCH: &str =
|
||||
"Position collateral vault does not match provided vault";
|
||||
pub(crate) const ERR_TOKEN_DEFINITION_MISMATCH: &str =
|
||||
"Token definition does not match the position's collateral definition";
|
||||
pub(crate) const ERR_TOKEN_DEFINITION_UNINITIALIZED: &str =
|
||||
"Collateral token definition must be initialized";
|
||||
pub(crate) const ERR_TOKEN_DEFINITION_INVALID: &str =
|
||||
"Collateral token definition must hold a valid TokenDefinition";
|
||||
pub(crate) const ERR_TOKEN_DEFINITION_NOT_FUNGIBLE: &str =
|
||||
"Collateral token definition must be fungible";
|
||||
pub(crate) const ERR_TOKEN_PROGRAM_MISMATCH: &str =
|
||||
"Collateral token definition, position vault, and user collateral holding must be owned by the same Token Program";
|
||||
pub(crate) const ERR_VAULT_INVALID_HOLDING: &str = "Vault account must hold a valid TokenHolding";
|
||||
pub(crate) const ERR_VAULT_WRONG_DEFINITION: &str =
|
||||
"Vault token holding is not for the position's collateral definition";
|
||||
pub(crate) const ERR_VAULT_NOT_FUNGIBLE: &str = "Position vault must be fungible";
|
||||
pub(crate) const ERR_USER_HOLDING_INVALID: &str =
|
||||
"User collateral holding must hold a valid TokenHolding";
|
||||
pub(crate) const ERR_USER_HOLDING_WRONG_DEFINITION: &str =
|
||||
"User collateral holding does not match the position's collateral definition";
|
||||
pub(crate) const ERR_USER_HOLDING_INSUFFICIENT_BALANCE: &str =
|
||||
"Deposit amount exceeds user collateral balance";
|
||||
pub(crate) const ERR_USER_HOLDING_NOT_FUNGIBLE: &str = "User collateral holding must be fungible";
|
||||
pub(crate) const ERR_COLLATERAL_OVERFLOW: &str = "Deposit amount overflows position collateral";
|
||||
|
||||
fn account_is_initialized(account: &Account) -> bool {
|
||||
// Runtime account claims assign a non-default owner; default-owned accounts are still
|
||||
// uninitialized for Stablecoin account validation even if other fields are non-default.
|
||||
account.program_owner != DEFAULT_PROGRAM_ID
|
||||
}
|
||||
|
||||
/// Deposit `amount` collateral tokens from `user_holding` into `position`'s vault.
|
||||
///
|
||||
/// Increases `Position.collateral_amount` by `amount` and emits a single chained
|
||||
/// `Token::Transfer` from the user holding to the vault. No collateralization
|
||||
/// check is required because debt is unchanged.
|
||||
/// [`token_core::Instruction::Transfer`] from the user holding to the vault when `amount` is
|
||||
/// nonzero. The token program is anchored to the collateral token definition, and the vault and
|
||||
/// user holding must be owned by that same program.
|
||||
/// Only the owner alignment state and updated position are returned as stablecoin post-states.
|
||||
/// Token-account balance post-states are produced by the chained transfer in the token program.
|
||||
/// No collateralization check is required because debt is unchanged.
|
||||
///
|
||||
/// # Panics
|
||||
/// - `owner` or `user_holding` is not authorized.
|
||||
@ -17,84 +63,110 @@ use token_core::TokenHolding;
|
||||
/// decode as a [`Position`], or sits at an address that does not match
|
||||
/// `compute_position_pda(stablecoin_program_id, owner, Position.collateral_definition_id)`.
|
||||
/// - `vault` is uninitialized, sits at an address that does not match
|
||||
/// `compute_position_vault_pda(stablecoin_program_id, position_id)`, or holds a [`TokenHolding`]
|
||||
/// whose `definition_id` does not match the position's collateral definition.
|
||||
/// - `user_holding` is uninitialized, owned by a different Token Program than the vault, or holds a
|
||||
/// [`TokenHolding`] whose `definition_id` does not match the position's collateral definition.
|
||||
/// `compute_position_vault_pda(stablecoin_program_id, position_id)`, is not owned by the
|
||||
/// collateral Token Program, holds a [`TokenHolding`] whose `definition_id` does not match the
|
||||
/// position's collateral definition, or is not fungible.
|
||||
/// - `user_holding` is uninitialized, owned by a different Token Program than the collateral
|
||||
/// definition, or holds a [`TokenHolding`] whose `definition_id` does not match the position's
|
||||
/// collateral definition, is not fungible, or has less than `amount` balance.
|
||||
/// - `token_definition` is uninitialized, does not match `Position.collateral_definition_id`, is
|
||||
/// owned by a different Token Program than the vault, does not hold a valid [`TokenDefinition`],
|
||||
/// or is not fungible.
|
||||
/// - `Position.collateral_amount + amount` overflows.
|
||||
pub fn deposit_collateral(
|
||||
owner: AccountWithMetadata,
|
||||
position: AccountWithMetadata,
|
||||
vault: AccountWithMetadata,
|
||||
user_holding: AccountWithMetadata,
|
||||
token_definition: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
amount: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(owner.is_authorized, "Owner authorization is missing");
|
||||
assert!(
|
||||
user_holding.is_authorized,
|
||||
"User collateral holding authorization is missing"
|
||||
);
|
||||
assert_ne!(
|
||||
position.account,
|
||||
Account::default(),
|
||||
"Position account must be initialized"
|
||||
);
|
||||
assert_eq!(
|
||||
position.account.program_owner, stablecoin_program_id,
|
||||
"Position is not owned by this stablecoin program"
|
||||
);
|
||||
assert_ne!(
|
||||
vault.account,
|
||||
Account::default(),
|
||||
"Vault must be initialized"
|
||||
);
|
||||
assert_ne!(
|
||||
user_holding.account,
|
||||
Account::default(),
|
||||
"User collateral holding must be initialized"
|
||||
);
|
||||
if !owner.is_authorized {
|
||||
panic!("{ERR_OWNER_AUTHORIZATION_MISSING}");
|
||||
}
|
||||
if !user_holding.is_authorized {
|
||||
panic!("{ERR_USER_HOLDING_AUTHORIZATION_MISSING}");
|
||||
}
|
||||
if !account_is_initialized(&position.account) {
|
||||
panic!("{ERR_POSITION_UNINITIALIZED}");
|
||||
}
|
||||
if position.account.program_owner != stablecoin_program_id {
|
||||
panic!("{ERR_POSITION_WRONG_PROGRAM_OWNER}");
|
||||
}
|
||||
if !account_is_initialized(&vault.account) {
|
||||
panic!("{ERR_VAULT_UNINITIALIZED}");
|
||||
}
|
||||
if !account_is_initialized(&user_holding.account) {
|
||||
panic!("{ERR_USER_HOLDING_UNINITIALIZED}");
|
||||
}
|
||||
|
||||
let position_data = Position::try_from(&position.account.data)
|
||||
.expect("Position account must hold valid Position state");
|
||||
let _position_seed = verify_position_and_get_seed(
|
||||
.unwrap_or_else(|error| panic!("{ERR_POSITION_INVALID_STATE}: {error:?}"));
|
||||
let _ = verify_position_and_get_seed(
|
||||
&position,
|
||||
&owner,
|
||||
position_data.collateral_definition_id,
|
||||
stablecoin_program_id,
|
||||
);
|
||||
let _vault_seed =
|
||||
verify_position_vault_and_get_seed(&vault, position.account_id, stablecoin_program_id);
|
||||
assert_eq!(
|
||||
position_data.collateral_vault_id, vault.account_id,
|
||||
"Position collateral vault does not match provided vault"
|
||||
);
|
||||
let _ = verify_position_vault_and_get_seed(&vault, position.account_id, stablecoin_program_id);
|
||||
if position_data.collateral_vault_id != vault.account_id {
|
||||
panic!("{ERR_POSITION_VAULT_MISMATCH}");
|
||||
}
|
||||
|
||||
if !account_is_initialized(&token_definition.account) {
|
||||
panic!("{ERR_TOKEN_DEFINITION_UNINITIALIZED}");
|
||||
}
|
||||
if token_definition.account_id != position_data.collateral_definition_id {
|
||||
panic!("{ERR_TOKEN_DEFINITION_MISMATCH}");
|
||||
}
|
||||
match TokenDefinition::try_from(&token_definition.account.data)
|
||||
.unwrap_or_else(|error| panic!("{ERR_TOKEN_DEFINITION_INVALID}: {error:?}"))
|
||||
{
|
||||
TokenDefinition::Fungible { .. } => {}
|
||||
TokenDefinition::NonFungible { .. } => panic!("{ERR_TOKEN_DEFINITION_NOT_FUNGIBLE}"),
|
||||
}
|
||||
|
||||
let token_program_id = token_definition.account.program_owner;
|
||||
if vault.account.program_owner != token_program_id {
|
||||
panic!("{ERR_TOKEN_PROGRAM_MISMATCH}");
|
||||
}
|
||||
|
||||
let vault_holding = TokenHolding::try_from(&vault.account.data)
|
||||
.expect("Vault account must hold a valid TokenHolding");
|
||||
assert_eq!(
|
||||
vault_holding.definition_id(),
|
||||
position_data.collateral_definition_id,
|
||||
"Vault token holding is not for the position's collateral definition"
|
||||
);
|
||||
.unwrap_or_else(|error| panic!("{ERR_VAULT_INVALID_HOLDING}: {error:?}"));
|
||||
if vault_holding.definition_id() != position_data.collateral_definition_id {
|
||||
panic!("{ERR_VAULT_WRONG_DEFINITION}");
|
||||
}
|
||||
match vault_holding {
|
||||
TokenHolding::Fungible { .. } => {}
|
||||
TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => {
|
||||
panic!("{ERR_VAULT_NOT_FUNGIBLE}");
|
||||
}
|
||||
}
|
||||
|
||||
let token_program_id = vault.account.program_owner;
|
||||
assert_eq!(
|
||||
user_holding.account.program_owner, token_program_id,
|
||||
"User collateral holding must be owned by same Token Program as the vault"
|
||||
);
|
||||
if user_holding.account.program_owner != token_program_id {
|
||||
panic!("{ERR_TOKEN_PROGRAM_MISMATCH}");
|
||||
}
|
||||
let user_holding_data = TokenHolding::try_from(&user_holding.account.data)
|
||||
.expect("User collateral holding must hold a valid TokenHolding");
|
||||
assert_eq!(
|
||||
user_holding_data.definition_id(),
|
||||
position_data.collateral_definition_id,
|
||||
"User collateral holding does not match the position's collateral definition"
|
||||
);
|
||||
.unwrap_or_else(|error| panic!("{ERR_USER_HOLDING_INVALID}: {error:?}"));
|
||||
if user_holding_data.definition_id() != position_data.collateral_definition_id {
|
||||
panic!("{ERR_USER_HOLDING_WRONG_DEFINITION}");
|
||||
}
|
||||
match user_holding_data {
|
||||
TokenHolding::Fungible { balance, .. } => {
|
||||
if balance < amount {
|
||||
panic!("{ERR_USER_HOLDING_INSUFFICIENT_BALANCE}");
|
||||
}
|
||||
}
|
||||
TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => {
|
||||
panic!("{ERR_USER_HOLDING_NOT_FUNGIBLE}");
|
||||
}
|
||||
}
|
||||
|
||||
let new_collateral = position_data
|
||||
.collateral_amount
|
||||
.checked_add(amount)
|
||||
.expect("Deposit amount overflows position collateral");
|
||||
.unwrap_or_else(|| panic!("{ERR_COLLATERAL_OVERFLOW}"));
|
||||
|
||||
let updated_position = Position {
|
||||
collateral_vault_id: position_data.collateral_vault_id,
|
||||
@ -108,10 +180,12 @@ pub fn deposit_collateral(
|
||||
let post_states = vec![
|
||||
AccountPostState::new(owner.account),
|
||||
AccountPostState::new(position_post),
|
||||
AccountPostState::new(vault.account.clone()),
|
||||
AccountPostState::new(user_holding.account.clone()),
|
||||
];
|
||||
|
||||
if amount == 0 {
|
||||
return (post_states, vec![]);
|
||||
}
|
||||
|
||||
let transfer_call = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![user_holding, vault],
|
||||
|
||||
@ -7,11 +7,12 @@
|
||||
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
||||
program::{ChainedCall, Claim, ProgramId},
|
||||
program::{AccountPostState, ChainedCall, Claim, ProgramId},
|
||||
};
|
||||
use stablecoin_core::{
|
||||
compute_position_pda, compute_position_pda_seed, compute_position_vault_pda,
|
||||
compute_position_vault_pda_seed, Position,
|
||||
compute_position_vault_pda_seed, Position, ERR_POSITION_ACCOUNT_ID_MISMATCH,
|
||||
ERR_POSITION_VAULT_ACCOUNT_ID_MISMATCH,
|
||||
};
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
|
||||
@ -176,6 +177,116 @@ fn user_stablecoin_holding_account(balance: u128) -> AccountWithMetadata {
|
||||
account
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_position_claims_pda_and_emits_chained_calls() {
|
||||
let collateral_amount: u128 = 500;
|
||||
@ -530,42 +641,47 @@ fn deposit_collateral_updates_position_and_emits_transfer() {
|
||||
let initial_debt: u128 = 300;
|
||||
let amount: u128 = 200;
|
||||
let holding_balance: u128 = 1_000;
|
||||
let position_account = init_position_account(initial_collateral, initial_debt);
|
||||
let vault = init_vault_account();
|
||||
let user_holding = user_holding_account(holding_balance);
|
||||
|
||||
let (post_states, chained_calls) = crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(initial_collateral, initial_debt),
|
||||
init_vault_account(),
|
||||
user_holding_account(holding_balance),
|
||||
position_account.clone(),
|
||||
vault.clone(),
|
||||
user_holding.clone(),
|
||||
collateral_definition_account(),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
amount,
|
||||
);
|
||||
|
||||
assert_eq!(post_states.len(), 4);
|
||||
assert_eq!(post_states.len(), 2);
|
||||
assert!(post_states
|
||||
.iter()
|
||||
.all(|post_state| post_state.account().program_owner != TOKEN_PROGRAM_ID));
|
||||
|
||||
let position_post = &post_states[1];
|
||||
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,
|
||||
);
|
||||
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: initial_debt,
|
||||
}
|
||||
);
|
||||
assert_eq!(position, expected_position);
|
||||
assert_eq!(position_post.account().program_owner, STABLECOIN_PROGRAM_ID);
|
||||
|
||||
assert_eq!(post_states[2].account(), &init_vault_account().account);
|
||||
assert_eq!(
|
||||
post_states[3].account(),
|
||||
&user_holding_account(holding_balance).account
|
||||
);
|
||||
|
||||
assert_eq!(chained_calls.len(), 1);
|
||||
let expected_transfer = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![user_holding_account(holding_balance), init_vault_account()],
|
||||
vec![user_holding, vault],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: amount,
|
||||
},
|
||||
@ -576,201 +692,377 @@ fn deposit_collateral_updates_position_and_emits_transfer() {
|
||||
#[test]
|
||||
fn deposit_collateral_allows_zero_amount() {
|
||||
let initial: u128 = 500;
|
||||
let position_account = init_position_account(initial, 0);
|
||||
let (post_states, chained_calls) = crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(initial, 0),
|
||||
position_account.clone(),
|
||||
init_vault_account(),
|
||||
user_holding_account(1_000),
|
||||
collateral_definition_account(),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
0,
|
||||
);
|
||||
let position = Position::try_from(&post_states[1].account().data).expect("valid Position");
|
||||
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");
|
||||
assert_eq!(position.collateral_amount, initial);
|
||||
|
||||
let expected_transfer = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![user_holding_account(1_000), init_vault_account()],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: 0,
|
||||
},
|
||||
);
|
||||
assert_eq!(chained_calls, vec![expected_transfer]);
|
||||
assert!(chained_calls.is_empty());
|
||||
}
|
||||
|
||||
#[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,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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]
|
||||
#[should_panic(expected = "Owner authorization is missing")]
|
||||
fn deposit_collateral_requires_owner_authorization() {
|
||||
let mut owner = owner_account();
|
||||
owner.is_authorized = false;
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner,
|
||||
init_position_account(500, 0),
|
||||
init_vault_account(),
|
||||
user_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
let mut fixture = deposit_fixture();
|
||||
fixture.owner.is_authorized = false;
|
||||
|
||||
assert_deposit_collateral_panics(
|
||||
fixture,
|
||||
crate::deposit_collateral::ERR_OWNER_AUTHORIZATION_MISSING,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "User collateral holding authorization is missing")]
|
||||
fn deposit_collateral_requires_user_holding_authorization() {
|
||||
let mut holding = user_holding_account(1_000);
|
||||
holding.is_authorized = false;
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(500, 0),
|
||||
init_vault_account(),
|
||||
holding,
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
let mut fixture = deposit_fixture();
|
||||
fixture.user_holding.is_authorized = false;
|
||||
|
||||
assert_deposit_collateral_panics(
|
||||
fixture,
|
||||
crate::deposit_collateral::ERR_USER_HOLDING_AUTHORIZATION_MISSING,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Position account must be initialized")]
|
||||
fn deposit_collateral_rejects_uninitialized_position() {
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
uninit_position_account(),
|
||||
init_vault_account(),
|
||||
user_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Position is not owned by this stablecoin program")]
|
||||
fn deposit_collateral_rejects_position_owned_by_other_program() {
|
||||
let mut position = init_position_account(500, 0);
|
||||
position.account.program_owner = [9u32; 8];
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
position,
|
||||
init_vault_account(),
|
||||
user_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Position account ID does not match expected derivation")]
|
||||
fn deposit_collateral_rejects_wrong_position_address() {
|
||||
let mut position = init_position_account(500, 0);
|
||||
position.account_id = AccountId::new([0xFFu8; 32]);
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
position,
|
||||
init_vault_account(),
|
||||
user_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
let mut fixture = deposit_fixture();
|
||||
fixture.position.account_id = AccountId::new([0xFFu8; 32]);
|
||||
|
||||
assert_deposit_collateral_panics(fixture, ERR_POSITION_ACCOUNT_ID_MISMATCH);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Position vault account ID does not match expected derivation")]
|
||||
fn deposit_collateral_rejects_wrong_vault_address() {
|
||||
let mut vault = init_vault_account();
|
||||
vault.account_id = AccountId::new([0xEEu8; 32]);
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(500, 0),
|
||||
vault,
|
||||
user_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Vault token holding is not for the position's collateral definition")]
|
||||
fn deposit_collateral_rejects_vault_for_other_definition() {
|
||||
let mut vault = init_vault_account();
|
||||
vault.account.data = Data::from(&TokenHolding::Fungible {
|
||||
let mut fixture = deposit_fixture();
|
||||
fixture.vault.account.data = Data::from(&TokenHolding::Fungible {
|
||||
definition_id: AccountId::new([0x21u8; 32]),
|
||||
balance: 0,
|
||||
});
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(500, 0),
|
||||
vault,
|
||||
user_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "User collateral holding must be initialized")]
|
||||
fn deposit_collateral_rejects_uninitialized_user_holding() {
|
||||
let holding = AccountWithMetadata {
|
||||
let mut fixture = deposit_fixture();
|
||||
fixture.user_holding = AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: true,
|
||||
account_id: user_holding_id(),
|
||||
};
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(500, 0),
|
||||
init_vault_account(),
|
||||
holding,
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "User collateral holding must be owned by same Token Program as the vault"
|
||||
)]
|
||||
fn deposit_collateral_rejects_holding_with_different_token_program() {
|
||||
let mut holding = user_holding_account(1_000);
|
||||
holding.account.program_owner = [9u32; 8];
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(500, 0),
|
||||
init_vault_account(),
|
||||
holding,
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "User collateral holding does not match the position's collateral definition"
|
||||
)]
|
||||
fn deposit_collateral_rejects_holding_for_other_definition() {
|
||||
let mut holding = user_holding_account(1_000);
|
||||
holding.account.data = Data::from(&TokenHolding::Fungible {
|
||||
let mut fixture = deposit_fixture();
|
||||
fixture.user_holding.account.data = Data::from(&TokenHolding::Fungible {
|
||||
definition_id: AccountId::new([0x21u8; 32]),
|
||||
balance: 1_000,
|
||||
});
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(500, 0),
|
||||
init_vault_account(),
|
||||
holding,
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
|
||||
assert_deposit_collateral_panics(
|
||||
fixture,
|
||||
crate::deposit_collateral::ERR_USER_HOLDING_WRONG_DEFINITION,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Deposit amount overflows position collateral")]
|
||||
fn deposit_collateral_rejects_collateral_overflow() {
|
||||
crate::deposit_collateral::deposit_collateral(
|
||||
owner_account(),
|
||||
init_position_account(u128::MAX, 0),
|
||||
init_vault_account(),
|
||||
user_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
1,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn withdraw_collateral_allows_full_drain() {
|
||||
let amount: u128 = 500;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user