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