Skip to main content

percli_program/instructions/
convert_released_pnl.rs

1use anchor_lang::prelude::*;
2use pyth_sdk_solana::state::{load_price_account, PriceStatus};
3
4use crate::error::{from_risk_error, PercolatorError};
5use crate::instructions::events;
6use crate::state::{engine_from_account_data, header_from_account_data, MARKET_ACCOUNT_SIZE};
7
8const MAX_PRICE_AGE_SECS: i64 = 60;
9const PYTH_PROGRAM_ID: Pubkey = pubkey!("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH");
10
11#[derive(Accounts)]
12pub struct ConvertReleasedPnl<'info> {
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    /// CHECK: Pyth price feed account.
24    #[account(
25        owner = PYTH_PROGRAM_ID @ PercolatorError::InvalidOraclePrice,
26    )]
27    pub oracle: UncheckedAccount<'info>,
28}
29
30pub fn handler(
31    ctx: Context<ConvertReleasedPnl>,
32    account_idx: u16,
33    x_req: u64,
34    funding_rate: i64,
35) -> Result<()> {
36    // Verify oracle matches header
37    {
38        let data = ctx.accounts.market.try_borrow_data()?;
39        require!(&data[0..8] == b"percmrkt", PercolatorError::AccountNotFound);
40        let header = header_from_account_data(&data)?;
41        require!(
42            header.oracle == ctx.accounts.oracle.key(),
43            PercolatorError::InvalidOraclePrice
44        );
45    }
46
47    // Read Pyth oracle price
48    let oracle_data = ctx.accounts.oracle.data.borrow();
49    let price_account = load_price_account::<32, ()>(&oracle_data)
50        .map_err(|_| error!(PercolatorError::InvalidOraclePrice))?;
51
52    require!(
53        price_account.agg.status == PriceStatus::Trading,
54        PercolatorError::InvalidOraclePrice
55    );
56
57    let clock = Clock::get()?;
58    let price_age = clock
59        .unix_timestamp
60        .checked_sub(price_account.timestamp)
61        .ok_or_else(|| error!(PercolatorError::StaleOracle))?;
62    require!(price_age <= MAX_PRICE_AGE_SECS, PercolatorError::StaleOracle);
63
64    let price = price_account.agg.price;
65    let expo = price_account.expo;
66
67    require!(price > 0, PercolatorError::InvalidOraclePriceValue);
68    require!(expo >= -18 && expo <= 18, PercolatorError::InvalidOraclePrice);
69
70    let oracle_price = if expo >= 0 {
71        (price as u64)
72            .checked_mul(10u64.pow(expo as u32))
73            .ok_or_else(|| error!(PercolatorError::InvalidOraclePriceValue))?
74    } else {
75        let divisor = 10u64.pow((-expo) as u32);
76        (price as u64)
77            .checked_div(divisor)
78            .ok_or_else(|| error!(PercolatorError::InvalidOraclePriceValue))?
79    };
80
81    require!(oracle_price > 0, PercolatorError::InvalidOraclePriceValue);
82
83    drop(oracle_data);
84
85    // Update engine
86    let market = &ctx.accounts.market;
87    let mut data = market.try_borrow_mut_data()?;
88
89    require!(
90        &data[0..8] == b"percmrkt",
91        PercolatorError::AccountNotFound
92    );
93
94    let engine = engine_from_account_data(&mut data);
95
96    engine
97        .convert_released_pnl_not_atomic(account_idx, x_req as u128, oracle_price, clock.slot, funding_rate)
98        .map_err(from_risk_error)?;
99
100    emit!(events::PnlConverted {
101        user: ctx.accounts.user.key(),
102        account_idx,
103        x_req,
104    });
105
106    Ok(())
107}