Skip to main content

percli_program/instructions/
withdraw.rs

1use anchor_lang::prelude::*;
2use anchor_spl::token::{transfer_checked, Mint, Token, TokenAccount, TransferChecked};
3
4use crate::error::{from_risk_error, PercolatorError};
5use crate::state::{
6    engine_from_account_data, header_from_account_data, market_signer_seeds, MARKET_ACCOUNT_SIZE,
7};
8
9#[derive(Accounts)]
10pub struct Withdraw<'info> {
11    #[account(mut)]
12    pub user: Signer<'info>,
13
14    /// CHECK: Validated via owner, discriminator, and size.
15    #[account(
16        mut,
17        owner = crate::ID @ PercolatorError::AccountNotFound,
18        constraint = market.data_len() == MARKET_ACCOUNT_SIZE @ PercolatorError::AccountNotFound,
19    )]
20    pub market: UncheckedAccount<'info>,
21
22    /// The collateral mint for this market.
23    pub mint: Account<'info, Mint>,
24
25    /// User's token account to receive withdrawn tokens.
26    #[account(
27        mut,
28        constraint = user_token_account.owner == user.key(),
29        constraint = user_token_account.mint == mint.key(),
30    )]
31    pub user_token_account: Account<'info, TokenAccount>,
32
33    /// Vault token account to transfer from.
34    #[account(
35        mut,
36        seeds = [b"vault", market.key().as_ref()],
37        bump,
38        constraint = vault.mint == mint.key(),
39    )]
40    pub vault: Account<'info, TokenAccount>,
41
42    pub token_program: Program<'info, Token>,
43}
44
45pub fn handler(
46    ctx: Context<Withdraw>,
47    account_idx: u16,
48    amount: u64,
49    funding_rate: i64,
50) -> Result<()> {
51    require!(amount > 0, PercolatorError::InsufficientBalance);
52
53    // Validate engine state and execute withdrawal (checks margin requirements)
54    let market = &ctx.accounts.market;
55    let mut data = market.try_borrow_mut_data()?;
56
57    require!(
58        &data[0..8] == b"percmrkt",
59        PercolatorError::AccountNotFound
60    );
61
62    let header = header_from_account_data(&data)?;
63    require!(header.mint == ctx.accounts.mint.key(), PercolatorError::Unauthorized);
64
65    let engine = engine_from_account_data(&mut data);
66
67    // Verify signer owns this account
68    let account_owner = engine.accounts[account_idx as usize].owner;
69    require!(
70        account_owner == ctx.accounts.user.key().to_bytes(),
71        PercolatorError::Unauthorized
72    );
73
74    let oracle_price = engine.last_oracle_price;
75    let clock = Clock::get()?;
76
77    // Engine validates margin requirements before allowing withdrawal
78    engine
79        .withdraw_not_atomic(account_idx, amount as u128, oracle_price, clock.slot, funding_rate)
80        .map_err(from_risk_error)?;
81
82    // Drop the borrow before CPI
83    drop(data);
84
85    // Transfer tokens from vault to user (signed by market PDA, which is vault authority)
86    let bump = [header.bump];
87    let signer_seeds = market_signer_seeds(&header.authority, &bump);
88    transfer_checked(
89        CpiContext::new_with_signer(
90            ctx.accounts.token_program.key(),
91            TransferChecked {
92                from: ctx.accounts.vault.to_account_info(),
93                to: ctx.accounts.user_token_account.to_account_info(),
94                authority: ctx.accounts.market.to_account_info(),
95                mint: ctx.accounts.mint.to_account_info(),
96            },
97            &[&signer_seeds],
98        ),
99        amount,
100        ctx.accounts.mint.decimals,
101    )?;
102
103    Ok(())
104}