mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-31 03:03:16 +00:00
parent
f4f61be322
commit
eb7f44a98a
@ -42,6 +42,41 @@
|
|||||||
"type": "u128"
|
"type": "u128"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withdraw_collateral",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "owner",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "position",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vault",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "destination",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "u128"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"accounts": [
|
"accounts": [
|
||||||
|
|||||||
@ -30,6 +30,26 @@ pub enum Instruction {
|
|||||||
/// Amount of collateral tokens to deposit into the position vault.
|
/// Amount of collateral tokens to deposit into the position vault.
|
||||||
collateral_amount: u128,
|
collateral_amount: u128,
|
||||||
},
|
},
|
||||||
|
/// Withdraw `amount` collateral tokens from a position back to a user-controlled holding.
|
||||||
|
///
|
||||||
|
/// Required accounts (4):
|
||||||
|
/// - Owner account (authorized)
|
||||||
|
/// - Position account (initialized, owned by `self_program_id`)
|
||||||
|
/// - Position vault token holding (address must match
|
||||||
|
/// `compute_position_vault_pda(self_program_id, position_id)`)
|
||||||
|
/// - Destination user collateral holding (initialized, owned by the vault's Token Program,
|
||||||
|
/// `TokenHolding.definition_id == Position.collateral_definition_id`)
|
||||||
|
///
|
||||||
|
/// `token_program_id` is derived from `vault.account.program_owner`;
|
||||||
|
/// `collateral_definition_id` is read from the decoded [`Position`].
|
||||||
|
///
|
||||||
|
/// **Note:** until issues #97/#96/#95 land, this instruction hard-asserts
|
||||||
|
/// `Position.debt_amount == 0` instead of accruing fees and checking the
|
||||||
|
/// collateralization ratio.
|
||||||
|
WithdrawCollateral {
|
||||||
|
/// Amount of collateral tokens to move from the vault back to `destination`.
|
||||||
|
amount: u128,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persistent state held by a Stablecoin [`Position`] account.
|
/// Persistent state held by a Stablecoin [`Position`] account.
|
||||||
|
|||||||
@ -41,4 +41,36 @@ mod stablecoin {
|
|||||||
chained_calls,
|
chained_calls,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Withdraw `amount` collateral tokens from an existing position back to a
|
||||||
|
/// user-controlled holding.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns the host program's panic-converted error if any precondition
|
||||||
|
/// fails (see
|
||||||
|
/// [`stablecoin_program::withdraw_collateral::withdraw_collateral`] for the
|
||||||
|
/// full list).
|
||||||
|
#[instruction]
|
||||||
|
pub fn withdraw_collateral(
|
||||||
|
ctx: ProgramContext,
|
||||||
|
owner: AccountWithMetadata,
|
||||||
|
position: AccountWithMetadata,
|
||||||
|
vault: AccountWithMetadata,
|
||||||
|
destination: AccountWithMetadata,
|
||||||
|
amount: u128,
|
||||||
|
) -> SpelResult {
|
||||||
|
let (post_states, chained_calls) =
|
||||||
|
stablecoin_program::withdraw_collateral::withdraw_collateral(
|
||||||
|
owner,
|
||||||
|
position,
|
||||||
|
vault,
|
||||||
|
destination,
|
||||||
|
ctx.self_program_id,
|
||||||
|
amount,
|
||||||
|
);
|
||||||
|
Ok(spel_framework::SpelOutput::execute(
|
||||||
|
post_states,
|
||||||
|
chained_calls,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,5 +5,8 @@ pub use stablecoin_core as core;
|
|||||||
/// Open a new collateral-only position for a calling owner.
|
/// Open a new collateral-only position for a calling owner.
|
||||||
pub mod open_position;
|
pub mod open_position;
|
||||||
|
|
||||||
|
/// Withdraw collateral from an existing position back to a user-controlled holding.
|
||||||
|
pub mod withdraw_collateral;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@ -99,6 +99,60 @@ fn uninit_vault_account() -> AccountWithMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destination_holding_account() -> AccountWithMetadata {
|
||||||
|
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: destination_holding_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_position_claims_pda_and_emits_chained_calls() {
|
fn open_position_claims_pda_and_emits_chained_calls() {
|
||||||
let collateral_amount: u128 = 500;
|
let collateral_amount: u128 = 500;
|
||||||
@ -392,3 +446,268 @@ fn position_pda_and_vault_pda_do_not_collide() {
|
|||||||
let vault = compute_position_vault_pda(STABLECOIN_PROGRAM_ID, position);
|
let vault = compute_position_vault_pda(STABLECOIN_PROGRAM_ID, position);
|
||||||
assert_ne!(position, vault);
|
assert_ne!(position, vault);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
129
stablecoin/src/withdraw_collateral.rs
Normal file
129
stablecoin/src/withdraw_collateral.rs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
use nssa_core::{
|
||||||
|
account::{Account, AccountWithMetadata, Data},
|
||||||
|
program::{AccountPostState, ChainedCall, ProgramId},
|
||||||
|
};
|
||||||
|
use stablecoin_core::{verify_position_and_get_seed, verify_position_vault_and_get_seed, Position};
|
||||||
|
use token_core::TokenHolding;
|
||||||
|
|
||||||
|
/// Withdraw `amount` collateral tokens from `position`'s vault back to `destination`.
|
||||||
|
///
|
||||||
|
/// Decreases `Position.collateral_amount` by `amount` and emits a single chained
|
||||||
|
/// `Token::Transfer` from the vault to `destination`, authorized by the vault
|
||||||
|
/// PDA seed. The position post-state uses plain [`AccountPostState::new`] —
|
||||||
|
/// the initial PDA claim already happened in
|
||||||
|
/// [`crate::open_position::open_position`].
|
||||||
|
///
|
||||||
|
/// Until issues #95 / #96 / #97 land (redemption price, price feed, stability
|
||||||
|
/// fee accrual), this instruction hard-asserts `Position.debt_amount == 0`.
|
||||||
|
/// When those land, this guard is replaced by real fee accrual + a
|
||||||
|
/// collateralization-ratio check against the post-withdrawal collateral.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// - `owner` is not authorized.
|
||||||
|
/// - `position` is uninitialized, not owned by `stablecoin_program_id`, holds data that does not
|
||||||
|
/// 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` 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.
|
||||||
|
/// - `destination` 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.
|
||||||
|
/// - `Position.debt_amount` is non-zero.
|
||||||
|
/// - `amount > Position.collateral_amount`.
|
||||||
|
pub fn withdraw_collateral(
|
||||||
|
owner: AccountWithMetadata,
|
||||||
|
position: AccountWithMetadata,
|
||||||
|
vault: AccountWithMetadata,
|
||||||
|
destination: AccountWithMetadata,
|
||||||
|
stablecoin_program_id: ProgramId,
|
||||||
|
amount: u128,
|
||||||
|
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||||
|
assert!(owner.is_authorized, "Owner 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"
|
||||||
|
);
|
||||||
|
|
||||||
|
let position_data = Position::try_from(&position.account.data)
|
||||||
|
.expect("Position account must hold valid Position state");
|
||||||
|
// `verify_position_and_get_seed` asserts the position address matches the
|
||||||
|
// (owner, collateral_definition) PDA derivation. We do not use the seed
|
||||||
|
// downstream — the position is already PDA-claimed.
|
||||||
|
let _position_seed = 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);
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
|
||||||
|
let token_program_id = vault.account.program_owner;
|
||||||
|
assert_ne!(
|
||||||
|
destination.account,
|
||||||
|
Account::default(),
|
||||||
|
"Destination must be initialized"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
destination.account.program_owner, token_program_id,
|
||||||
|
"Destination must be owned by the same Token Program as the vault"
|
||||||
|
);
|
||||||
|
let destination_holding = TokenHolding::try_from(&destination.account.data)
|
||||||
|
.expect("Destination account must hold a valid TokenHolding");
|
||||||
|
assert_eq!(
|
||||||
|
destination_holding.definition_id(),
|
||||||
|
position_data.collateral_definition_id,
|
||||||
|
"Destination token definition does not match the position's collateral definition"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
position_data.debt_amount, 0,
|
||||||
|
"withdraw_collateral with debt is not supported yet — stability fee accrual and collateralization check land with #97/#96"
|
||||||
|
);
|
||||||
|
let new_collateral = position_data
|
||||||
|
.collateral_amount
|
||||||
|
.checked_sub(amount)
|
||||||
|
.expect("Withdrawal amount exceeds position collateral");
|
||||||
|
|
||||||
|
let updated_position = Position {
|
||||||
|
collateral_vault_id: position_data.collateral_vault_id,
|
||||||
|
collateral_definition_id: position_data.collateral_definition_id,
|
||||||
|
collateral_amount: new_collateral,
|
||||||
|
debt_amount: position_data.debt_amount,
|
||||||
|
};
|
||||||
|
let mut position_post = position.account.clone();
|
||||||
|
position_post.data = Data::from(&updated_position);
|
||||||
|
|
||||||
|
let post_states = vec![
|
||||||
|
AccountPostState::new(owner.account),
|
||||||
|
AccountPostState::new(position_post),
|
||||||
|
AccountPostState::new(vault.account.clone()),
|
||||||
|
AccountPostState::new(destination.account.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut vault_authorized = vault.clone();
|
||||||
|
vault_authorized.is_authorized = true;
|
||||||
|
let transfer_call = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![vault_authorized, destination],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![vault_seed]);
|
||||||
|
|
||||||
|
(post_states, vec![transfer_call])
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user