diff --git a/hxro-print-trade-provider/program/src/constants.rs b/hxro-print-trade-provider/program/src/constants.rs index 9a02dc27..ddc9c511 100644 --- a/hxro-print-trade-provider/program/src/constants.rs +++ b/hxro-print-trade-provider/program/src/constants.rs @@ -2,6 +2,7 @@ use dex::utils::numeric::Fractional; pub const CONFIG_SEED: &str = "config"; pub const OPERATOR_SEED: &str = "operator"; +pub const LOCKED_COLLATERAL_RECORD_SEED: &str = "locked_collateral_record"; pub const MAX_PRODUCTS_PER_TRADE: usize = 6; pub const EXPECTED_DECIMALS: u8 = 9; diff --git a/hxro-print-trade-provider/program/src/errors.rs b/hxro-print-trade-provider/program/src/errors.rs index f5ef5f1a..fd044e6c 100644 --- a/hxro-print-trade-provider/program/src/errors.rs +++ b/hxro-print-trade-provider/program/src/errors.rs @@ -56,4 +56,6 @@ pub enum HxroPrintTradeProviderError { InvalidPrintTradeAddress, #[msg("Invalid print trade parameters")] InvalidPrintTradeParams, + #[msg("Only a lock record creator can remove it")] + NotALockCreator, } diff --git a/hxro-print-trade-provider/program/src/helpers/conversions.rs b/hxro-print-trade-provider/program/src/helpers/conversions.rs index 8b0c178e..2943c2af 100644 --- a/hxro-print-trade-provider/program/src/helpers/conversions.rs +++ b/hxro-print-trade-provider/program/src/helpers/conversions.rs @@ -4,7 +4,7 @@ use agnostic_orderbook::state::Side; use dex::utils::numeric::Fractional; use rfq::state::{AuthoritySide, Response, Rfq}; -use crate::state::ParsedLegData; +use crate::state::{FractionalCopy, ParsedLegData, ProductInfo}; use super::common::{get_leg_instrument_type, parse_leg_data}; @@ -15,24 +15,24 @@ pub fn to_hxro_side(side: AuthoritySide) -> Side { } } -pub struct ProductInfo { - pub product_index: u64, - pub size: Fractional, -} - -pub fn to_hxro_product(rfq: &Rfq, response: &Response, leg_index: u8) -> Result { +pub fn to_hxro_product( + perspective: AuthoritySide, + rfq: &Rfq, + response: &Response, + leg_index: u8, +) -> Result { let leg = &rfq.legs[leg_index as usize]; let instrument_type = get_leg_instrument_type(leg)?; let (_, ParsedLegData { product_index }) = parse_leg_data(leg, instrument_type)?; let mut amount = response.get_leg_amount_to_transfer(&rfq, leg_index) as i64; - if response.get_leg_assets_receiver(rfq, leg_index) == AuthoritySide::Maker { + if response.get_leg_assets_receiver(rfq, leg_index) == perspective.inverse() { amount = -amount; } let result = ProductInfo { product_index: product_index as u64, - size: Fractional { + size: FractionalCopy { m: amount, exp: leg.amount_decimals as u64, }, diff --git a/hxro-print-trade-provider/program/src/helpers/create_print_trade.rs b/hxro-print-trade-provider/program/src/helpers/create_print_trade.rs index 03aa7712..27d8447a 100644 --- a/hxro-print-trade-provider/program/src/helpers/create_print_trade.rs +++ b/hxro-print-trade-provider/program/src/helpers/create_print_trade.rs @@ -6,14 +6,15 @@ use dex::{cpi::accounts::InitializePrintTrade, state::print_trade::PrintTradePro use rfq::state::AuthoritySide; use crate::constants::OPERATOR_SEED; +use crate::state::ProductInfo; use crate::{ constants::{OPERATOR_COUNTERPARTY_FEE_PROPORTION, OPERATOR_CREATOR_FEE_PROPORTION}, PreparePrintTradeAccounts, }; +use super::conversions::to_hxro_price; use super::conversions::to_hxro_product; use super::conversions::to_hxro_side; -use super::conversions::{to_hxro_price, ProductInfo}; pub fn initialize_print_trade<'info>( ctx: &Context<'_, '_, '_, 'info, PreparePrintTradeAccounts<'info>>, @@ -43,10 +44,10 @@ pub fn initialize_print_trade<'info>( let ProductInfo { product_index, size, - } = to_hxro_product(rfq, response, i as u8)?; + } = to_hxro_product(AuthoritySide::Taker, rfq, response, i as u8)?; products[i] = PrintTradeProductIndex { product_index: product_index as usize, - size, + size: size.into(), }; } let params = InitializePrintTradeParams { diff --git a/hxro-print-trade-provider/program/src/helpers/mod.rs b/hxro-print-trade-provider/program/src/helpers/mod.rs index 3ffb28f2..fc590c78 100644 --- a/hxro-print-trade-provider/program/src/helpers/mod.rs +++ b/hxro-print-trade-provider/program/src/helpers/mod.rs @@ -7,6 +7,7 @@ mod initialize_trader_risk_group; mod validation; pub use close_print_trade::*; +pub use conversions::*; pub use create_print_trade::*; pub use execute_print_trade::*; pub use initialize_trader_risk_group::*; diff --git a/hxro-print-trade-provider/program/src/lib.rs b/hxro-print-trade-provider/program/src/lib.rs index cd8b75d1..3e690dcb 100644 --- a/hxro-print-trade-provider/program/src/lib.rs +++ b/hxro-print-trade-provider/program/src/lib.rs @@ -1,18 +1,18 @@ use anchor_lang::prelude::*; -use constants::{CONFIG_SEED, OPERATOR_SEED}; +use constants::{CONFIG_SEED, LOCKED_COLLATERAL_RECORD_SEED, OPERATOR_SEED}; use dex::state::market_product_group::MarketProductGroup; use dex::state::print_trade::{PrintTrade, PrintTradeExecutionOutput}; use dex::{program::Dex, state::trader_risk_group::TraderRiskGroup}; use rfq::interfaces::print_trade_provider::SettlementResult; use rfq::state::{ProtocolState, Response, Rfq}; -use state::Config; +use state::{Config, LockedCollateralRecord, ProductInfo}; // use dex_cpi::instruction::*; use errors::HxroPrintTradeProviderError; use helpers::{ close_print_trade, execute_print_trade, initialize_print_trade, initialize_trader_risk_group, - validate_print_trade_accounts, ValidationInput, + to_hxro_product, validate_print_trade_accounts, ValidationInput, }; use state::AuthoritySideDuplicate; @@ -31,6 +31,8 @@ declare_id!("GyRW7qvzx6UTVW9DkQGMy5f1rp9XK2x53FvWSjUUF7BJ"); #[program] pub mod hxro_print_trade_provider { + use crate::state::FractionalCopy; + use super::*; pub fn initialize_config( @@ -52,6 +54,12 @@ pub mod hxro_print_trade_provider { initialize_trader_risk_group(ctx) } + pub fn remove_locked_collateral_record( + _ctx: Context, + ) -> Result<()> { + Ok(()) + } + pub fn validate_print_trade(ctx: Context) -> Result<()> { let ValidatePrintTradeAccounts { rfq, @@ -152,6 +160,22 @@ pub mod hxro_print_trade_provider { } else { initialize_print_trade(&ctx, authority_side.into())?; } + + let mut locks = [ProductInfo { + product_index: 0, + size: FractionalCopy { m: 0, exp: 0 }, + }; 6]; + for i in 0..rfq.legs.len() { + locks[i] = to_hxro_product(authority_side.into(), rfq, response, i as u8)?; + } + ctx.accounts + .locked_collateral_record + .set_inner(LockedCollateralRecord { + user: user.key(), + response: response.key(), + locks, + }); + Ok(()) } @@ -245,6 +269,15 @@ pub struct ModifyConfigAccounts<'info> { pub config: Account<'info, Config>, } +#[derive(Accounts)] +pub struct RemoveLockedCollateralRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + + #[account(mut, close=user, constraint = locked_collateral_record.user == user.key() @ HxroPrintTradeProviderError::NotALockCreator)] + pub locked_collateral_record: Account<'info, LockedCollateralRecord>, +} + #[derive(Accounts)] pub struct InitializeOperatorTraderRiskGroupAccounts<'info> { #[account(mut, constraint = protocol.authority == authority.key() @ HxroPrintTradeProviderError::NotAProtocolAuthority)] @@ -310,6 +343,14 @@ pub struct PreparePrintTradeAccounts<'info> { pub rfq: Box>, pub response: Box>, + #[account( + init, + payer = user, + seeds = [LOCKED_COLLATERAL_RECORD_SEED.as_bytes(), user.key().as_ref(), response.key().as_ref()], + space = 8 + LockedCollateralRecord::INIT_SPACE, + bump + )] + pub locked_collateral_record: Account<'info, LockedCollateralRecord>, /// CHECK PDA account #[account(seeds = [OPERATOR_SEED.as_bytes()], bump)] pub operator: UncheckedAccount<'info>, @@ -317,6 +358,7 @@ pub struct PreparePrintTradeAccounts<'info> { pub dex: Program<'info, Dex>, #[account(constraint = config.valid_mpg == market_product_group.key() @ HxroPrintTradeProviderError::NotAValidatedMpg)] pub market_product_group: AccountLoader<'info, MarketProductGroup>, + #[account(mut)] pub user: Signer<'info>, pub taker_trg: AccountLoader<'info, TraderRiskGroup>, pub maker_trg: AccountLoader<'info, TraderRiskGroup>, diff --git a/hxro-print-trade-provider/program/src/state.rs b/hxro-print-trade-provider/program/src/state.rs index eb335835..1cc1b39f 100644 --- a/hxro-print-trade-provider/program/src/state.rs +++ b/hxro-print-trade-provider/program/src/state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use dex::utils::numeric::Fractional; use rfq::state::AuthoritySide; #[account] @@ -7,6 +8,36 @@ pub struct Config { pub valid_mpg: Pubkey, } +#[account] +#[derive(InitSpace)] +pub struct LockedCollateralRecord { + pub user: Pubkey, + pub response: Pubkey, + pub locks: [ProductInfo; 6], +} + +#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq, Default, InitSpace)] +pub struct ProductInfo { + pub product_index: u64, + pub size: FractionalCopy, +} + +#[repr(C)] +#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq, Default, InitSpace)] +pub struct FractionalCopy { + pub m: i64, + pub exp: u64, +} + +impl From for Fractional { + fn from(value: FractionalCopy) -> Self { + Self { + m: value.m, + exp: value.exp, + } + } +} + // Duplicate required because anchor doesn't generate IDL for imported structs #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq)] pub enum AuthoritySideDuplicate { diff --git a/tests/integration/hxro.spec.ts b/tests/integration/hxro.spec.ts index a645c99d..155ddc26 100644 --- a/tests/integration/hxro.spec.ts +++ b/tests/integration/hxro.spec.ts @@ -217,8 +217,8 @@ describe("RFQ HXRO instrument integration tests", () => { expect(responseData.defaultingParty).to.be.deep.equal(AuthoritySide.Taker); await response.settleOnePartyDefault(); - await response.revertPrintTradeSettlementPreparation(AuthoritySide.Taker); - await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker); + await response.revertPrintTradeSettlementPreparation(AuthoritySide.Taker, { skipPreStep: true }); + await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker, { skipPreStep: true }); await response.cleanUp(); }); @@ -246,8 +246,8 @@ describe("RFQ HXRO instrument integration tests", () => { expect(responseData.defaultingParty).to.be.deep.equal(AuthoritySide.Maker); await response.settleOnePartyDefault(); - await response.revertPrintTradeSettlementPreparation(AuthoritySide.Taker); - await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker); + await response.revertPrintTradeSettlementPreparation(AuthoritySide.Taker, { skipPreStep: true }); + await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker, { skipPreStep: true }); await response.cleanUp(); }); diff --git a/tests/utilities/printTradeProviders/hxroPrintTradeProvider.ts b/tests/utilities/printTradeProviders/hxroPrintTradeProvider.ts index 26c55673..098dfd41 100644 --- a/tests/utilities/printTradeProviders/hxroPrintTradeProvider.ts +++ b/tests/utilities/printTradeProviders/hxroPrintTradeProvider.ts @@ -18,13 +18,14 @@ export const DEFAULT_SETTLEMENT_OUTCOME = { price: "100", legs: ["-10"] }; const configSeed = "config"; const operatorSeed = "operator"; +const lockedCollateralRecordSeed = "locked_collateral_record"; const trgSize = 64336; export const toHxroAmount = (value: number) => value * 10 ** hxroDecimals; let hxroPrintTradeProviderProgram: Program | null = null; -export function getHxroInstrumentProgram(): Program { +export function getHxroProviderProgram(): Program { if (hxroPrintTradeProviderProgram === null) { hxroPrintTradeProviderProgram = workspace.HxroPrintTradeProvider as Program; } @@ -52,11 +53,11 @@ export class HxroPrintTradeProvider { ) {} static async addPrintTradeProvider(context: Context) { - await context.addPrintTradeProvider(getHxroInstrumentProgram().programId, 2, true); + await context.addPrintTradeProvider(getHxroProviderProgram().programId, 2, true); } static async initializeConfig(context: Context, validMpg: PublicKey) { - await getHxroInstrumentProgram() + await getHxroProviderProgram() .methods.initializeConfig(validMpg) .accounts({ authority: context.dao.publicKey, @@ -100,7 +101,7 @@ export class HxroPrintTradeProvider { lamports: 1 * 10 ** 9, // 1 sol }); - await getHxroInstrumentProgram() + await getHxroProviderProgram() .methods.initializeOperatorTraderRiskGroup() .accounts({ authority: context.dao.publicKey, @@ -124,19 +125,28 @@ export class HxroPrintTradeProvider { } static getConfigAddress() { - const program = getHxroInstrumentProgram(); + const program = getHxroProviderProgram(); const [address] = PublicKey.findProgramAddressSync([Buffer.from(configSeed)], program.programId); return address; } static getOperatorAddress() { - const program = getHxroInstrumentProgram(); + const program = getHxroProviderProgram(); const [address] = PublicKey.findProgramAddressSync([Buffer.from(operatorSeed)], program.programId); return address; } + static getLockedCollateralRecordAddress(user: PublicKey, response: PublicKey) { + const program = getHxroProviderProgram(); + const [address] = PublicKey.findProgramAddressSync( + [Buffer.from(lockedCollateralRecordSeed), user.toBuffer(), response.toBuffer()], + program.programId + ); + return address; + } + getProgramId(): PublicKey { - return getHxroInstrumentProgram().programId; + return getHxroProviderProgram().programId; } getLegData(): LegData[] { @@ -220,24 +230,48 @@ export class HxroPrintTradeProvider { } } - async manageCollateral(action: "lock" | "unlock", side: AuthoritySide, expectedSettlement: SettlementOutcome) { + async executePreRevertPrintTradeSettlementPreparation(side: AuthoritySide, rfq: Rfq, response: Response) { const { taker, maker } = this.context; - const { mpg, trgTaker, trgMaker, latestDexProgram, riskAndFeeSigner } = this.hxroContext; - const [user, userTrg] = side == AuthoritySide.Taker ? [taker, trgTaker] : [maker, trgMaker]; + const user = side == AuthoritySide.Taker ? taker : maker; + const lockRecord = HxroPrintTradeProvider.getLockedCollateralRecordAddress(user.publicKey, response.account); + const lockRecordData = await getHxroProviderProgram().account.lockedCollateralRecord.fetch(lockRecord); + + const locksByLegs = lockRecordData.locks.map((x) => x.size); + + await this.manageCollateralByLocks("unlock", side, locksByLegs); + + await getHxroProviderProgram() + .methods.removeLockedCollateralRecord() + .accountsStrict({ + user: user.publicKey, + lockedCollateralRecord: lockRecord, + }) + .signers([user]) + .rpc(); + } + async manageCollateral(action: "lock" | "unlock", side: AuthoritySide, expectedSettlement: SettlementOutcome) { if (side === AuthoritySide.Maker) { expectedSettlement = inverseExpectedSettlement(expectedSettlement); } const fractionalSettlement = convertExpectedSettlementToFractional(expectedSettlement); + await this.manageCollateralByLocks(action, side, fractionalSettlement.legs); + } + + async manageCollateralByLocks(action: "lock" | "unlock", side: AuthoritySide, locksByLegs: { m: BN; exp: BN }[]) { + const { taker, maker } = this.context; + const { mpg, trgTaker, trgMaker, latestDexProgram, riskAndFeeSigner } = this.hxroContext; + const [user, userTrg] = side == AuthoritySide.Taker ? [taker, trgTaker] : [maker, trgMaker]; + const products = []; for (let i = 0; i < 6; i++) { if (i < this.legs.length) { const leg = this.legs[i]; products.push({ productIndex: new BN(leg.productIndex), - size: fractionalSettlement.legs[i], + size: locksByLegs[i], }); } else { products.push({ productIndex: new BN(0), size: { m: new BN(0), exp: new BN(0) } }); @@ -390,6 +424,11 @@ export class HxroPrintTradeProvider { return [ { pubkey: this.getProgramId(), isSigner: false, isWritable: false }, + { + pubkey: HxroPrintTradeProvider.getLockedCollateralRecordAddress(user, response.account), + isSigner: false, + isWritable: true, + }, { pubkey: HxroPrintTradeProvider.getOperatorAddress(), isSigner: false, isWritable: false }, { pubkey: HxroPrintTradeProvider.getConfigAddress(), isSigner: false, isWritable: false }, { pubkey: dexProgram.programId, isSigner: false, isWritable: false }, diff --git a/tests/utilities/wrappers.ts b/tests/utilities/wrappers.ts index e84c9b10..a6739bc3 100644 --- a/tests/utilities/wrappers.ts +++ b/tests/utilities/wrappers.ts @@ -1329,11 +1329,15 @@ export class Response { .rpc(); } - async revertPrintTradeSettlementPreparation(side: { taker: {} } | { maker: {} }) { + async revertPrintTradeSettlementPreparation(side: { taker: {} } | { maker: {} }, { skipPreStep = false } = {}) { if (this.rfq.content.type != "printTradeProvider") { throw Error("Not settled by print trade!"); } + if (!skipPreStep) { + await this.rfq.content.provider.executePreRevertPrintTradeSettlementPreparation(side, this.rfq, this); + } + const remainingAccounts = this.rfq.content.provider.getRevertPrintTradeSettlementPreparationAccounts( this.rfq, this