gmsol_store/instructions/exchange/
position_cut.rs

1use std::ops::Deref;
2
3use anchor_lang::prelude::*;
4use anchor_spl::{
5    associated_token::AssociatedToken,
6    token::{Mint, Token, TokenAccount},
7};
8use gmsol_utils::InitSpace;
9
10use crate::{
11    check_delegation, constants,
12    events::{EventEmitter, TradeData, TradeEventRef},
13    get_pnl_token,
14    ops::{
15        execution_fee::PayExecutionFeeOperation,
16        order::{PositionCutKind, PositionCutOperation},
17    },
18    states::{
19        common::action::ActionExt,
20        feature::{ActionDisabledFlag, DomainDisabledFlag},
21        order::Order,
22        user::UserHeader,
23        Chainlink, HasMarketMeta, Market, NonceBytes, Oracle, Position, Seed, Store,
24        TokenMapHeader,
25    },
26    utils::internal,
27    validated_recent_timestamp, CoreError,
28};
29
30/// The accounts definitions for the [`liquidate`](crate::gmsol_store::liquidate) and
31/// [`auto_deleverage`](crate::gmsol_store::auto_deleverage) instructions.
32///
33/// Remaining accounts expected by this instruction:
34///
35///   - 0..N. `[]` N feed accounts, where N represents the total number of unique tokens
36///     in the market.
37///
38/// # Warnings
39/// Because token accounts can be frozen by token's
40/// [freeze authority](https://explorer.solana.com/address/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/metadata)
41/// (although extremely unlikely), keepers may need to be aware of the following two issues when using the
42/// "position cut" instructions:
43///
44/// ## Escrow Accounts Are Frozen
45///
46/// If escrow accounts are frozen, the "position cut" instruction will fail to execute due to a transfer failure.
47///
48/// However, token accounts cannot be frozen before they are initialized. To avoid this issue, the keeper only needs
49/// to ensure that the escrow accounts creation instruction and the "position cut" instruction are within the same
50/// transaction.
51///
52/// ## Claimable Accounts Are Frozen
53///
54/// If the funding fee of a position is paid with secondary output tokens (pnl tokens), it may generate claimable
55/// funds belonging to the holding address. Alternatively, if a negative position's price impact is too high,
56/// it may generate a `price_impact_diff` that results in claimable funds transferred to the owner.
57/// If claimable funds are generated and the corresponding claimable accounts are frozen at
58/// this time, the "position cut" instruction may also fail to execute due to transfer failure.
59///
60/// Since claimable accounts may be created before executing the "position cut" instruction, in such cases, the keeper
61/// needs to wait until new claimable accounts can be created (when the recent time window changes, causing the address
62/// to change) before re-executing the "position cut" instruction.
63///
64/// In the future, we may avoid directly using token accounts to receive these claimable funds to prevent this issue.
65#[event_cpi]
66#[derive(Accounts)]
67#[instruction(nonce: [u8; 32], recent_timestamp: i64)]
68pub struct PositionCut<'info> {
69    /// Authority.
70    #[account(mut)]
71    pub authority: Signer<'info>,
72    /// The owner of the position.
73    /// CHECK: only used to receive fund.
74    #[account(mut)]
75    pub owner: UncheckedAccount<'info>,
76    /// User Account.
77    #[account(
78        mut,
79        constraint = user.load()?.is_initialized() @ CoreError::InvalidUserAccount,
80        has_one = owner,
81        has_one = store,
82        seeds = [UserHeader::SEED, store.key().as_ref(), owner.key().as_ref()],
83        bump = user.load()?.bump,
84    )]
85    pub user: AccountLoader<'info, UserHeader>,
86    /// Store.
87    #[account(mut, has_one = token_map)]
88    pub store: AccountLoader<'info, Store>,
89    /// Token map.
90    #[account(has_one = store)]
91    pub token_map: AccountLoader<'info, TokenMapHeader>,
92    /// Buffer for oracle prices.
93    #[account(mut, has_one = store)]
94    pub oracle: AccountLoader<'info, Oracle>,
95    /// Market.
96    #[account(mut, has_one = store)]
97    pub market: AccountLoader<'info, Market>,
98    /// The order to be created.
99    #[account(
100        init,
101        space = 8 + Order::INIT_SPACE,
102        payer = authority,
103        seeds = [Order::SEED, store.key().as_ref(), authority.key().as_ref(), &nonce],
104        bump,
105    )]
106    pub order: AccountLoader<'info, Order>,
107    #[account(
108        mut,
109        constraint = position.load()?.owner == owner.key(),
110        constraint = position.load()?.store == store.key(),
111        seeds = [
112            Position::SEED,
113            store.key().as_ref(),
114            owner.key().as_ref(),
115            position.load()?.market_token.as_ref(),
116            position.load()?.collateral_token.as_ref(),
117            &[position.load()?.kind],
118        ],
119        bump = position.load()?.bump,
120    )]
121    pub position: AccountLoader<'info, Position>,
122    /// Trade event buffer.
123    #[account(mut, has_one = store, has_one = authority)]
124    pub event: AccountLoader<'info, TradeData>,
125    /// Long token.
126    pub long_token: Box<Account<'info, Mint>>,
127    /// Short token.
128    pub short_token: Box<Account<'info, Mint>>,
129    /// The escrow account for long tokens.
130    #[account(
131        mut,
132        associated_token::mint = long_token,
133        associated_token::authority = order,
134    )]
135    pub long_token_escrow: Box<Account<'info, TokenAccount>>,
136    /// The escrow account for short tokens.
137    #[account(
138        mut,
139        associated_token::mint = short_token,
140        associated_token::authority = order,
141    )]
142    pub short_token_escrow: Box<Account<'info, TokenAccount>>,
143    /// Long token vault.
144    #[account(
145        mut,
146        token::mint = long_token,
147        token::authority = store,
148        seeds = [
149            constants::MARKET_VAULT_SEED,
150            store.key().as_ref(),
151            long_token_vault.mint.as_ref(),
152        ],
153        bump,
154    )]
155    pub long_token_vault: Box<Account<'info, TokenAccount>>,
156    /// Short token vault.
157    #[account(
158        mut,
159        token::mint = short_token,
160        token::authority = store,
161        seeds = [
162            constants::MARKET_VAULT_SEED,
163            store.key().as_ref(),
164            short_token_vault.mint.as_ref(),
165        ],
166        bump,
167    )]
168    pub short_token_vault: Box<Account<'info, TokenAccount>>,
169    #[account(
170        mut,
171        token::mint = market.load()?.meta().long_token_mint,
172        token::authority = store,
173        constraint = check_delegation(&claimable_long_token_account_for_user, position.load()?.owner)?,
174        seeds = [
175            constants::CLAIMABLE_ACCOUNT_SEED,
176            store.key().as_ref(),
177            market.load()?.meta().long_token_mint.as_ref(),
178            position.load()?.owner.as_ref(),
179            &store.load()?.claimable_time_key(validated_recent_timestamp(store.load()?.deref(), recent_timestamp)?)?,
180        ],
181        bump,
182    )]
183    pub claimable_long_token_account_for_user: Box<Account<'info, TokenAccount>>,
184    #[account(
185        mut,
186        token::mint = market.load()?.meta().short_token_mint,
187        token::authority = store,
188        constraint = check_delegation(&claimable_short_token_account_for_user, position.load()?.owner)?,
189        seeds = [
190            constants::CLAIMABLE_ACCOUNT_SEED,
191            store.key().as_ref(),
192            market.load()?.meta().short_token_mint.as_ref(),
193            position.load()?.owner.as_ref(),
194            &store.load()?.claimable_time_key(validated_recent_timestamp(store.load()?.deref(), recent_timestamp)?)?,
195        ],
196        bump,
197    )]
198    pub claimable_short_token_account_for_user: Box<Account<'info, TokenAccount>>,
199    #[account(
200        mut,
201        token::mint = get_pnl_token(&Some(position.clone()), market.load()?.deref())?,
202        token::authority = store,
203        constraint = check_delegation(&claimable_pnl_token_account_for_holding, store.load()?.address.holding)?,
204        seeds = [
205            constants::CLAIMABLE_ACCOUNT_SEED,
206            store.key().as_ref(),
207            get_pnl_token(&Some(position.clone()), market.load()?.deref())?.as_ref(),
208            store.load()?.address.holding.as_ref(),
209            &store.load()?.claimable_time_key(validated_recent_timestamp(store.load()?.deref(), recent_timestamp)?)?,
210        ],
211        bump,
212    )]
213    pub claimable_pnl_token_account_for_holding: Box<Account<'info, TokenAccount>>,
214    /// Initial collatearl token vault.
215    /// The system program.
216    pub system_program: Program<'info, System>,
217    /// The token program.
218    pub token_program: Program<'info, Token>,
219    /// The associated token program.
220    pub associated_token_program: Program<'info, AssociatedToken>,
221    /// Chainlink Program.
222    pub chainlink_program: Option<Program<'info, Chainlink>>,
223}
224
225/// CHECK: only ORDER_KEEPER is allowed to use this instrcution.
226pub(crate) fn unchecked_process_position_cut<'info>(
227    mut ctx: Context<'_, '_, 'info, 'info, PositionCut<'info>>,
228    nonce: &NonceBytes,
229    _recent_timestamp: i64,
230    kind: PositionCutKind,
231    execution_fee: u64,
232    should_unwrap_native_token: bool,
233) -> Result<()> {
234    let accounts = &mut ctx.accounts;
235
236    // Validate feature enabled.
237    {
238        let store = accounts.store.load()?;
239        let domain = match kind {
240            PositionCutKind::Liquidate => DomainDisabledFlag::Liquidation,
241            PositionCutKind::AutoDeleverage(_) => DomainDisabledFlag::AutoDeleveraging,
242        };
243        store.validate_feature_enabled(domain, ActionDisabledFlag::Create)?;
244        store.validate_feature_enabled(domain, ActionDisabledFlag::Execute)?;
245    }
246
247    let remaining_accounts = ctx.remaining_accounts;
248
249    let (tokens, is_pure_market) = {
250        let market = accounts.market.load()?;
251        let meta = market.meta();
252        (
253            meta.ordered_tokens().into_iter().collect::<Vec<_>>(),
254            meta.is_pure(),
255        )
256    };
257
258    let refund = match kind {
259        PositionCutKind::Liquidate => Order::position_cut_rent(is_pure_market, true)?,
260        // For fairness, the keeper will not be refunded the execution fee for ADL.
261        PositionCutKind::AutoDeleverage(_) => Order::position_cut_rent(is_pure_market, false)?,
262    };
263
264    let event_emitter = EventEmitter::new(&accounts.event_authority, ctx.bumps.event_authority);
265
266    let ops = PositionCutOperation::builder()
267        .kind(kind)
268        .position(&accounts.position)
269        .order(&accounts.order)
270        .event(&accounts.event)
271        .market(&accounts.market)
272        .store(&accounts.store)
273        .owner(accounts.owner.to_account_info())
274        .user(&accounts.user)
275        .nonce(nonce)
276        .order_bump(ctx.bumps.order)
277        .long_token_mint(&accounts.long_token)
278        .short_token_mint(&accounts.short_token)
279        .long_token_account(&accounts.long_token_escrow)
280        .long_token_vault(&accounts.long_token_vault)
281        .short_token_account(&accounts.short_token_escrow)
282        .short_token_vault(&accounts.short_token_vault)
283        .claimable_long_token_account_for_user(
284            accounts
285                .claimable_long_token_account_for_user
286                .to_account_info(),
287        )
288        .claimable_short_token_account_for_user(
289            accounts
290                .claimable_short_token_account_for_user
291                .to_account_info(),
292        )
293        .claimable_pnl_token_account_for_holding(
294            accounts
295                .claimable_pnl_token_account_for_holding
296                .to_account_info(),
297        )
298        .token_program(accounts.token_program.to_account_info())
299        .system_program(accounts.system_program.to_account_info())
300        // CHECK: the address of `order` has been checked to be derived from this account's address.
301        .executor(accounts.authority.to_account_info())
302        .refund(refund)
303        .should_unwrap_native_token(should_unwrap_native_token)
304        .event_emitter(event_emitter);
305
306    let should_send_trade_event = accounts.oracle.load_mut()?.with_prices(
307        &accounts.store,
308        &accounts.token_map,
309        &tokens,
310        remaining_accounts,
311        accounts.chainlink_program.as_ref(),
312        |oracle, _remaining_accounts| ops.oracle(oracle).build().execute(),
313    )?;
314
315    if should_send_trade_event {
316        let event_loader = accounts.event.clone();
317        let event = event_loader.load()?;
318        let event = TradeEventRef::from(&*event);
319        event_emitter.emit_cpi(&event)?;
320    }
321
322    accounts.pay_execution_fee(execution_fee)?;
323    Ok(())
324}
325
326impl<'info> internal::Authentication<'info> for PositionCut<'info> {
327    fn authority(&self) -> &Signer<'info> {
328        &self.authority
329    }
330
331    fn store(&self) -> &AccountLoader<'info, Store> {
332        &self.store
333    }
334}
335
336impl PositionCut<'_> {
337    #[inline(never)]
338    fn pay_execution_fee(&self, execution_fee: u64) -> Result<()> {
339        let execution_lamports = self.order.load()?.execution_lamports(execution_fee);
340        PayExecutionFeeOperation::builder()
341            .payer(self.order.to_account_info())
342            .receiver(self.authority.to_account_info())
343            .execution_lamports(execution_lamports)
344            .build()
345            .execute()?;
346        Ok(())
347    }
348}