@@ -7,7 +7,8 @@ use anchor_spl::{
77use crate :: constants:: { POOL_SEED , POSITION_SEED , VAULT_SEED } ;
88use crate :: errors:: PerpError ;
99use crate :: instructions:: shared:: {
10- basis_points_of, refresh_price_and_funding_within_band, settle_position,
10+ apply_haircut, basis_points_of, credit_fee, haircut_ratio, position_pnl,
11+ refresh_price_and_funding_within_band, settle_position,
1112} ;
1213use crate :: state:: { Pool , Position } ;
1314
@@ -18,15 +19,36 @@ pub fn handle_close_position(
1819 let pool = & mut context. accounts . pool ;
1920 let price = refresh_price_and_funding_within_band ( pool, & context. accounts . oracle_feed ) ?;
2021
22+ // The haircut is computed while this position is still in the per-side
23+ // accumulators, so its own profit counts toward the liability and it is
24+ // paid the same fraction as any other winner closing at this price. Its
25+ // own profit is passed too: if open losers offset it in the aggregate,
26+ // the haircut is sized against that profit, so the payout is at most the
27+ // backing and the close is never refused for lack of it.
2128 let position = & context. accounts . position ;
29+ let closing_profit = position_pnl ( position. side , position. size , position. entry_price , price) ?;
30+ let haircut = haircut_ratio ( pool, price, closing_profit) ?;
31+
2232 let position_size = position. size ;
33+ let entry_slot = position. entry_slot ;
2334 let settlement = settle_position ( pool, position, price) ?;
2435 let close_fee = basis_points_of ( position_size, pool. close_fee_bps ) ?;
2536
26- // Recoverable profit is capped at the reserved amount (the position's
27- // notional `size`), so the pool can always cover a winner. Losses are not
28- // capped.
29- let realized_pnl = settlement. profit_and_loss . min ( position_size as i128 ) ;
37+ // A profit is paid only once the position has been open for the pool's
38+ // warm-up, and then only the haircut fraction of it. A loss settles in
39+ // full, at any time.
40+ let realized_pnl = if settlement. profit_and_loss > 0 {
41+ let matured_at = entry_slot
42+ . checked_add ( pool. profit_warmup_slots )
43+ . ok_or ( PerpError :: MathOverflow ) ?;
44+ require ! (
45+ Clock :: get( ) ?. slot >= matured_at,
46+ PerpError :: ProfitNotMatured
47+ ) ;
48+ apply_haircut ( settlement. profit_and_loss , haircut) ?
49+ } else {
50+ settlement. profit_and_loss
51+ } ;
3052 let equity = settlement
3153 . equity
3254 . checked_sub ( settlement. profit_and_loss )
@@ -44,29 +66,35 @@ pub fn handle_close_position(
4466 let payout: u64 = payout. try_into ( ) . map_err ( |_| PerpError :: MathOverflow ) ?;
4567 require ! ( payout >= minimum_payout, PerpError :: SlippageExceeded ) ;
4668
47- // Release the position's reserved liquidity now that it is closing.
48- pool. reserved_liquidity = pool
49- . reserved_liquidity
50- . checked_sub ( position_size)
51- . ok_or ( PerpError :: MathOverflow ) ?;
52-
53- // Liquidity providers are the counterparty: they pay the trader's (capped)
54- // profit and receive their loss, and collect the funding the trader owed.
69+ // Liquidity providers are the counterparty: they pay the trader's
70+ // haircut profit and receive their loss, and collect the funding the
71+ // trader owed. The part of a profit the haircut withholds stays in
72+ // `liquidity`. A payment larger than `liquidity` takes the rest from the
73+ // insurance fund, which the haircut counted as backing. The haircut keeps
74+ // the profit within both; `PoolInsolvent` remains as a defensive check.
5575 let liquidity_delta = settlement
5676 . funding
5777 . checked_sub ( realized_pnl)
5878 . ok_or ( PerpError :: MathOverflow ) ?;
5979 let new_liquidity = ( pool. liquidity as i128 )
6080 . checked_add ( liquidity_delta)
6181 . ok_or ( PerpError :: MathOverflow ) ?;
62- require ! ( new_liquidity >= 0 , PerpError :: PoolInsolvent ) ;
63- pool. liquidity = new_liquidity
64- . try_into ( )
65- . map_err ( |_| PerpError :: MathOverflow ) ?;
66- pool. program_fees = pool
67- . program_fees
68- . checked_add ( close_fee)
69- . ok_or ( PerpError :: MathOverflow ) ?;
82+ if new_liquidity < 0 {
83+ let shortfall: u64 = new_liquidity
84+ . unsigned_abs ( )
85+ . try_into ( )
86+ . map_err ( |_| PerpError :: MathOverflow ) ?;
87+ pool. insurance_fund = pool
88+ . insurance_fund
89+ . checked_sub ( shortfall)
90+ . ok_or ( PerpError :: PoolInsolvent ) ?;
91+ pool. liquidity = 0 ;
92+ } else {
93+ pool. liquidity = new_liquidity
94+ . try_into ( )
95+ . map_err ( |_| PerpError :: MathOverflow ) ?;
96+ }
97+ credit_fee ( pool, close_fee) ?;
7098
7199 // The pool signs the CPI below with its own seeds.
72100 let pool_seeds: & [ & [ u8 ] ] = & [
0 commit comments