Skip to main content

acta_maker_sdk/chain/vault/
settle.rs

1use super::pda;
2use crate::chain::ix::{
3    ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID,
4    derive_associated_token_address as ata,
5};
6use crate::{
7    PositionType,
8    chain::{ChainError, MarketInfo, PositionInfo, PositionStatus},
9};
10use solana_sdk::{
11    instruction::{AccountMeta, Instruction},
12    pubkey::Pubkey,
13};
14
15/// Settles a position the maker side of: on a sell the maker holds (a vault may write); on a
16/// buy the maker or a vault writes and a wallet holds. `maker_owner` signs and pays rent.
17pub fn build_settle_position_ix(
18    program_id: Pubkey,
19    maker_owner: Pubkey,
20    position: &PositionInfo,
21    market: &MarketInfo,
22) -> Result<Instruction, ChainError> {
23    let buy = position.side.is_taker_buy();
24    require_maker_side(maker_owner, position)?;
25    if position.terms.is_spread() {
26        return crate::chain::spreads::build_cash_settle_ix(
27            program_id,
28            maker_owner,
29            position,
30            market,
31        );
32    }
33    if position.flags & !7 != 0 || position.flags2 != 0 || position.flags3 != 0 {
34        return Err(ChainError::InvalidAccountData);
35    }
36    let market_pda = crate::quote::market_pda(
37        &program_id,
38        &market.underlying_mint,
39        &market.quote_mint,
40        market.expiry_ts,
41        market.is_put,
42    );
43    if market_pda != position.market_pda {
44        return Err(ChainError::Identity("position market"));
45    }
46    if !market.is_finalized
47        || market.is_put != (position.terms.position_type() == PositionType::CashSecuredPut)
48    {
49        return Err(ChainError::InvalidSettlementState);
50    }
51    for program in [
52        market.underlying_token_program_id,
53        market.quote_token_program_id,
54    ] {
55        if program != TOKEN_PROGRAM_ID && program != TOKEN_2022_PROGRAM_ID {
56            return Err(ChainError::InvalidAccountData);
57        }
58    }
59    let otm = is_otm(position, market);
60    let open = position.status == PositionStatus::Open;
61    // An Open sell that finishes in the money is liquidated, not settled.
62    if position.status != PositionStatus::Funded && !(otm || buy) {
63        return Err(ChainError::InvalidSettlementState);
64    }
65    let Destinations {
66        collateral,
67        settlement,
68        writer,
69        holder,
70    } = destinations(position, market);
71    let position_pda = pda::position(&program_id, position.order_id.as_bytes()).0;
72    let mut accounts = vec![
73        AccountMeta::new(maker_owner, true),
74        AccountMeta::new(position_pda, false),
75        AccountMeta::new(market_pda, false),
76        AccountMeta::new(ata(&position_pda, &collateral.0, &collateral.1), false),
77        if open {
78            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false)
79        } else {
80            AccountMeta::new(ata(&position_pda, &settlement.0, &settlement.1), false)
81        },
82        AccountMeta::new(ata(&position.writer, &writer.0, &writer.1), false),
83        if open {
84            AccountMeta::new_readonly(ASSOCIATED_TOKEN_PROGRAM_ID, false)
85        } else {
86            AccountMeta::new(ata(&position.holder, &holder.0, &holder.1), false)
87        },
88        AccountMeta::new_readonly(market.underlying_token_program_id, false),
89        AccountMeta::new_readonly(market.quote_token_program_id, false),
90    ];
91    if position.flags & 1 != 0 {
92        accounts.extend([
93            AccountMeta::new_readonly(market.underlying_mint, false),
94            AccountMeta::new_readonly(market.quote_mint, false),
95            AccountMeta::new(position.writer, false),
96        ]);
97    }
98    Ok(Instruction {
99        program_id,
100        accounts,
101        data: vec![10, 0],
102    })
103}
104
105pub fn settle_position_sequence(
106    program_id: Pubkey,
107    maker_owner: Pubkey,
108    position: &PositionInfo,
109    market: &MarketInfo,
110) -> Result<Vec<Instruction>, ChainError> {
111    if position.terms.is_spread() {
112        require_maker_side(maker_owner, position)?;
113        return crate::chain::spreads::cash_settle_sequence(
114            program_id,
115            maker_owner,
116            position,
117            market,
118        );
119    }
120    let settle = build_settle_position_ix(program_id, maker_owner, position, market)?;
121    let Destinations { writer, holder, .. } = destinations(position, market);
122    let mut instructions = Vec::with_capacity(3);
123    // Each wallet that receives tokens needs its ATA; a vault side pays into its own legs.
124    let funded = position.status == PositionStatus::Funded;
125    if position.flags & 1 == 0 && (funded || position.side.is_taker_buy()) {
126        instructions.push(super::lifecycle::create_destination_ata(
127            maker_owner,
128            position.writer,
129            writer.0,
130            writer.1,
131        ));
132    }
133    if funded {
134        instructions.push(super::lifecycle::create_destination_ata(
135            maker_owner,
136            position.holder,
137            holder.0,
138            holder.1,
139        ));
140    }
141    instructions.push(settle);
142    Ok(instructions)
143}
144
145/// The maker side: the holder on a sell, the writer on a buy. A vault writer may be settled by
146/// any signer; a vault holder settles through the vault route instead.
147fn require_maker_side(maker_owner: Pubkey, position: &PositionInfo) -> Result<(), ChainError> {
148    let ok = if position.side.is_taker_buy() {
149        position.flags & 1 != 0 || position.writer == maker_owner
150    } else {
151        position.flags & 2 == 0 && position.holder == maker_owner
152    };
153    if ok {
154        Ok(())
155    } else {
156        Err(ChainError::Identity("wallet maker owner"))
157    }
158}
159
160fn is_otm(position: &PositionInfo, market: &MarketInfo) -> bool {
161    if market.is_put {
162        market.settlement_price >= position.terms.strike().value()
163    } else {
164        market.settlement_price <= position.terms.strike().value()
165    }
166}
167
168struct Destinations {
169    collateral: (Pubkey, Pubkey),
170    settlement: (Pubkey, Pubkey),
171    /// Mint and token program the writer receives.
172    writer: (Pubkey, Pubkey),
173    /// Mint and token program the holder receives when funded.
174    holder: (Pubkey, Pubkey),
175}
176
177/// Mirrors settle: the writer gets collateral back when OTM or when a buy was never funded.
178fn destinations(position: &PositionInfo, market: &MarketInfo) -> Destinations {
179    let underlying = (market.underlying_mint, market.underlying_token_program_id);
180    let quote = (market.quote_mint, market.quote_token_program_id);
181    let (collateral, settlement) = if market.is_put {
182        (quote, underlying)
183    } else {
184        (underlying, quote)
185    };
186    let otm = is_otm(position, market);
187    let unfunded_buy = position.side.is_taker_buy() && position.status == PositionStatus::Open;
188    Destinations {
189        collateral,
190        settlement,
191        writer: if otm || unfunded_buy {
192            collateral
193        } else {
194            settlement
195        },
196        holder: if otm { settlement } else { collateral },
197    }
198}
199
200#[cfg(feature = "chain-rpc")]
201impl crate::chain::ChainClient {
202    pub async fn build_settle_position(
203        &self,
204        maker_owner: Pubkey,
205        position_pda: Pubkey,
206    ) -> Result<Instruction, ChainError> {
207        let (position, market) = self.settlement_snapshots(position_pda).await?;
208        build_settle_position_ix(self.program_id(), maker_owner, &position, &market)
209    }
210
211    pub async fn settle_position_sequence(
212        &self,
213        maker_owner: Pubkey,
214        position_pda: Pubkey,
215    ) -> Result<Vec<Instruction>, ChainError> {
216        let (position, market) = self.settlement_snapshots(position_pda).await?;
217        settle_position_sequence(self.program_id(), maker_owner, &position, &market)
218    }
219
220    async fn settlement_snapshots(
221        &self,
222        position_pda: Pubkey,
223    ) -> Result<(PositionInfo, MarketInfo), ChainError> {
224        let position = self.fetch_position_info(&position_pda).await?;
225        if pda::position(&self.program_id(), position.order_id.as_bytes()).0 != position_pda {
226            return Err(ChainError::Identity("position PDA"));
227        }
228        let market = self.fetch_market_info(&position.market_pda).await?;
229        Ok((position, market))
230    }
231}