Skip to content

Commit

Permalink
Expand hxro lock record functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
EquilateralDelta committed Dec 13, 2023
1 parent 3ae342c commit 8dd29a2
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 30 deletions.
8 changes: 8 additions & 0 deletions hxro-print-trade-provider/program/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,12 @@ pub enum HxroPrintTradeProviderError {
InvalidPrintTradeParams,
#[msg("Only a lock record creator can remove it")]
NotALockCreator,
#[msg("Not a valid taker account")]
NotATaker,
#[msg("Not a valid maker account")]
NotAMaker,
#[msg("Can't remove a collateral lock record for a live settlement")]
RecordIsInUse,
#[msg("Invalid collateral lock record address")]
InvalidLockAddress,
}
80 changes: 70 additions & 10 deletions hxro-print-trade-provider/program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ mod state;

declare_id!("GyRW7qvzx6UTVW9DkQGMy5f1rp9XK2x53FvWSjUUF7BJ");

// Hxro global TODOS
// 2 points in the pinned message
// 2 bugs from the chat
// print trade address conflict
// trgs checking

#[program]
pub mod hxro_print_trade_provider {
use crate::state::FractionalCopy;
Expand All @@ -55,8 +49,13 @@ pub mod hxro_print_trade_provider {
}

pub fn remove_locked_collateral_record(
_ctx: Context<RemoveLockedCollateralRecord>,
ctx: Context<RemoveLockedCollateralRecord>,
) -> Result<()> {
require!(
!ctx.accounts.locked_collateral_record.is_in_use,
HxroPrintTradeProviderError::RecordIsInUse
);

Ok(())
}

Expand Down Expand Up @@ -173,7 +172,9 @@ pub mod hxro_print_trade_provider {
.set_inner(LockedCollateralRecord {
user: user.key(),
response: response.key(),
is_in_use: true,
locks,
reserved: [0; 64],
});

Ok(())
Expand All @@ -185,6 +186,10 @@ pub mod hxro_print_trade_provider {
let SettlePrintTradeAccounts {
rfq,
response,
taker,
maker,
taker_locked_collateral_record,
maker_locked_collateral_record,
taker_trg,
maker_trg,
operator,
Expand All @@ -204,13 +209,47 @@ pub mod hxro_print_trade_provider {
print_trade_key: print_trade.key(),
})?;

execute_print_trade(&ctx)
let settlement_result = execute_print_trade(&ctx)?;

if settlement_result == SettlementResult::Success {
taker_locked_collateral_record.close(taker.to_account_info())?;
maker_locked_collateral_record.close(maker.to_account_info())?;
}

Ok(settlement_result)
}

pub fn revert_print_trade_preparation<'info>(
_ctx: Context<'_, '_, '_, 'info, RevertPrintTradePreparationAccounts<'info>>,
_authority_side: AuthoritySideDuplicate,
ctx: Context<'_, '_, '_, 'info, RevertPrintTradePreparationAccounts<'info>>,
authority_side: AuthoritySideDuplicate,
) -> Result<()> {
let RevertPrintTradePreparationAccounts {
rfq,
response,
locked_collateral_record,
..
} = ctx.accounts;

let lock_owner = match authority_side {
AuthoritySideDuplicate::Taker => rfq.taker,
AuthoritySideDuplicate::Maker => response.maker,
};
let (expected_lock_address, _) = Pubkey::find_program_address(
&[
LOCKED_COLLATERAL_RECORD_SEED.as_bytes(),
lock_owner.as_ref(),
response.key().as_ref(),
],
&ID,
);
require_keys_eq!(
locked_collateral_record.key(),
expected_lock_address,
HxroPrintTradeProviderError::InvalidLockAddress
);

locked_collateral_record.is_in_use = false;

Ok(())
}

Expand Down Expand Up @@ -376,6 +415,24 @@ pub struct SettlePrintTradeAccounts<'info> {
pub rfq: Box<Account<'info, Rfq>>,
pub response: Box<Account<'info, Response>>,

/// CHECK: is a taker account
#[account(mut, constraint = rfq.taker == taker.key() @ HxroPrintTradeProviderError::NotATaker)]
pub taker: UncheckedAccount<'info>,
/// CHECK: is a maker account
#[account(mut, constraint = response.maker == maker.key() @ HxroPrintTradeProviderError::NotAMaker)]
pub maker: UncheckedAccount<'info>,
#[account(
mut,
seeds = [LOCKED_COLLATERAL_RECORD_SEED.as_bytes(), rfq.taker.as_ref(), response.key().as_ref()],
bump
)]
pub taker_locked_collateral_record: Box<Account<'info, LockedCollateralRecord>>,
#[account(
mut,
seeds = [LOCKED_COLLATERAL_RECORD_SEED.as_bytes(), response.maker.as_ref(), response.key().as_ref()],
bump
)]
pub maker_locked_collateral_record: Box<Account<'info, LockedCollateralRecord>>,
/// CHECK PDA account
#[account(seeds = [OPERATOR_SEED.as_bytes()], bump)]
pub operator: UncheckedAccount<'info>,
Expand Down Expand Up @@ -421,6 +478,9 @@ pub struct RevertPrintTradePreparationAccounts<'info> {
pub protocol: Box<Account<'info, ProtocolState>>,
pub rfq: Box<Account<'info, Rfq>>,
pub response: Box<Account<'info, Response>>,

#[account(mut)]
pub locked_collateral_record: Account<'info, LockedCollateralRecord>,
}

#[derive(Accounts)]
Expand Down
2 changes: 2 additions & 0 deletions hxro-print-trade-provider/program/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ pub struct Config {
pub struct LockedCollateralRecord {
pub user: Pubkey,
pub response: Pubkey,
pub is_in_use: bool,
pub locks: [ProductInfo; 6],
pub reserved: [u8; 64],
}

#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq, Default, InitSpace)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ fn validate(ctx: &Context<FinalizeRfqConstructionAccounts>) -> Result<()> {
ProtocolError::LegsSizeDoesNotMatchExpectedSize
);

let legs_serialized = rfq.legs.try_to_vec().unwrap();
let legs_hash = solana_program::hash::hash(&legs_serialized);
let legs_hash = solana_program::hash::hash(&serialized_legs);
require!(
legs_hash.to_bytes() == rfq.expected_legs_hash,
ProtocolError::LegsHashDoesNotMatchExpectedHash
Expand Down
2 changes: 1 addition & 1 deletion rfq/program/src/interfaces/print_trade_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const SETTLE_PRINT_TRADE_SELECTOR: [u8; 8] = [188, 110, 242, 145, 117, 203, 30,
const REVERT_PRINT_TRADE_PREPARATION_SELECTOR: [u8; 8] = [242, 33, 96, 69, 184, 244, 78, 6];
const CLEAN_UP_PRINT_TRADE_SELECTOR: [u8; 8] = [246, 29, 115, 215, 20, 227, 25, 57];

#[derive(AnchorSerialize, AnchorDeserialize)]
#[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Eq)]
pub enum SettlementResult {
Success,
TakerDefaults,
Expand Down
4 changes: 2 additions & 2 deletions rfq/program/src/state/rfq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ pub struct Rfq {
pub cleared_responses: u32,
pub confirmed_responses: u32,

pub print_trade_provider: Option<Pubkey>, // move higher after replacing with nullable wrapper
pub reserved: [u8; 256],

pub legs: Vec<Leg>, // TODO add limit for this size
pub print_trade_provider: Option<Pubkey>, // move higher after replacing with nullable wrapper
pub legs: Vec<Leg>, // TODO add limit for this size
}

impl Rfq {
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/hxro.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, { skipPreStep: true });
await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker, { skipPreStep: true });
await response.revertPrintTradeSettlementPreparation(AuthoritySide.Taker);
await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker);
await response.cleanUp();
});

Expand Down Expand Up @@ -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, { skipPreStep: true });
await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker, { skipPreStep: true });
await response.revertPrintTradeSettlementPreparation(AuthoritySide.Taker);
await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker);
await response.cleanUp();
});

Expand Down
120 changes: 120 additions & 0 deletions tests/unit/hxroCollateralLockedRecords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Context, getContext } from "../utilities/wrappers";
import { attachImprovedLogDisplay, expectError } from "../utilities/helpers";
import {
HxroPrintTradeProvider,
HxroContext,
getHxroContext,
DEFAULT_SETTLEMENT_OUTCOME,
getHxroProviderProgram,
} from "../utilities/printTradeProviders/hxroPrintTradeProvider";
import { AuthoritySide, Quote, QuoteSide } from "../utilities/types";
import {
DEFAULT_LEG_MULTIPLIER,
DEFAULT_LEG_SIDE,
DEFAULT_PRICE,
SOLANA_BASE_ASSET_INDEX,
} from "../utilities/constants";
import { expect } from "chai";

describe("RFQ HXRO collateral lock records", () => {
beforeEach(function () {
attachImprovedLogDisplay(this, context);
});

let context: Context;
let hxroContext: HxroContext;

before(async () => {
context = await getContext();
hxroContext = await getHxroContext(context);
});

it("Successful settlement removes locks", async () => {
const printTradeProvider = new HxroPrintTradeProvider(context, hxroContext);
const rfq = await context.createPrintTradeRfq({
printTradeProvider,
});
const response = await rfq.respond();
await response.confirm();
await response.preparePrintTradeSettlement(AuthoritySide.Taker, DEFAULT_SETTLEMENT_OUTCOME);
await response.preparePrintTradeSettlement(AuthoritySide.Maker, DEFAULT_SETTLEMENT_OUTCOME);

const hxroProgram = getHxroProviderProgram();
const takerLockAddress = HxroPrintTradeProvider.getLockedCollateralRecordAddress(
context.taker.publicKey,
response.account
);
const makerLockAddress = HxroPrintTradeProvider.getLockedCollateralRecordAddress(
context.maker.publicKey,
response.account
);
const [takerLockBefore, makerLockBefore] = await hxroProgram.account.lockedCollateralRecord.fetchMultiple([
takerLockAddress,
makerLockAddress,
]);
await response.settlePrintTrade();
const [takerLockAfter, makerLockAfter] = await hxroProgram.account.lockedCollateralRecord.fetchMultiple([
takerLockAddress,
makerLockAddress,
]);

expect(takerLockBefore).to.be.not.equal(null);
expect(makerLockBefore).to.be.not.equal(null);
expect(takerLockAfter).to.be.equal(null);
expect(makerLockAfter).to.be.equal(null);
});

it("Successfully remove lock records after a failed settlement", async () => {
const printTradeProvider = new HxroPrintTradeProvider(context, hxroContext, [
{ amount: 200, side: DEFAULT_LEG_SIDE, baseAssetIndex: SOLANA_BASE_ASSET_INDEX, productIndex: 0 },
]);
const rfq = await context.createPrintTradeRfq({
printTradeProvider,
});
const response = await rfq.respond({ ask: Quote.getStandard(DEFAULT_PRICE, DEFAULT_LEG_MULTIPLIER) });
await response.confirm({ side: QuoteSide.Ask });

const expectedSettlement = { price: "-100", legs: ["200"] };

await response.preparePrintTradeSettlement(AuthoritySide.Taker, expectedSettlement);
await printTradeProvider.manageCollateral("unlock", AuthoritySide.Taker, expectedSettlement);
await response.preparePrintTradeSettlement(AuthoritySide.Maker, expectedSettlement);
await response.settlePrintTrade();
await response.settleOnePartyDefault();
await response.revertPrintTradeSettlementPreparation(AuthoritySide.Taker);
await response.revertPrintTradeSettlementPreparation(AuthoritySide.Maker);

await printTradeProvider.unlockCollateralAndRemoveRecord(AuthoritySide.Maker, rfq, response);

const hxroProgram = getHxroProviderProgram();
const makerLockAddress = HxroPrintTradeProvider.getLockedCollateralRecordAddress(
context.maker.publicKey,
response.account
);
const makerLock = await hxroProgram.account.lockedCollateralRecord.fetchNullable(makerLockAddress);
expect(makerLock).to.be.equal(null);
});

it("Can't remove record while in use", async () => {
const printTradeProvider = new HxroPrintTradeProvider(context, hxroContext, [
{ amount: 300, side: DEFAULT_LEG_SIDE, baseAssetIndex: SOLANA_BASE_ASSET_INDEX, productIndex: 0 },
]);
const rfq = await context.createPrintTradeRfq({
printTradeProvider,
});
const response = await rfq.respond({ ask: Quote.getStandard(DEFAULT_PRICE, DEFAULT_LEG_MULTIPLIER) });
await response.confirm({ side: QuoteSide.Ask });

const expectedSettlement = { price: "-100", legs: ["300"] };

await response.preparePrintTradeSettlement(AuthoritySide.Taker, expectedSettlement);
await printTradeProvider.manageCollateral("unlock", AuthoritySide.Taker, expectedSettlement);
await response.preparePrintTradeSettlement(AuthoritySide.Maker, expectedSettlement);
await response.settlePrintTrade();

await expectError(
printTradeProvider.unlockCollateralAndRemoveRecord(AuthoritySide.Maker, rfq, response),
"RecordIsInUse"
);
});
});
2 changes: 0 additions & 2 deletions tests/utilities/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export const QUOTE_ESCROW_SEED = "quote_escrow";
export const BASE_ASSET_INFO_SEED = "base_asset";
export const MINT_INFO_SEED = "mint_info";

export const EMPTY_LEG_SIZE = 32 + 2 + 4 + 8 + 1 + 1 + 64;

export const LEG_MULTIPLIER_DECIMALS = 9;
export const ABSOLUTE_PRICE_DECIMALS = 9;
export const FEE_BPS_DECIMALS = 9;
Expand Down
30 changes: 27 additions & 3 deletions tests/utilities/printTradeProviders/hxroPrintTradeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export class HxroPrintTradeProvider {
}
}

async executePreRevertPrintTradeSettlementPreparation(side: AuthoritySide, rfq: Rfq, response: Response) {
async unlockCollateralAndRemoveRecord(side: AuthoritySide, rfq: Rfq, response: Response) {
const { taker, maker } = this.context;
const user = side == AuthoritySide.Taker ? taker : maker;
const lockRecord = HxroPrintTradeProvider.getLockedCollateralRecordAddress(user.publicKey, response.account);
Expand Down Expand Up @@ -443,6 +443,10 @@ export class HxroPrintTradeProvider {
}

getExecutePrintTradeSettlementAccounts(rfq: Rfq, response: Response) {
const {
taker: { publicKey: taker },
maker: { publicKey: maker },
} = this.context;
const { mpg, trgTaker, trgMaker, trgOperator, dexProgram, executionOutput, riskAndFeeSigner } = this.hxroContext;

const [creatorTrg, counterpartyTrg] = response.firstToPrepare?.equals(this.context.taker.publicKey)
Expand All @@ -456,6 +460,18 @@ export class HxroPrintTradeProvider {

return [
{ pubkey: this.getProgramId(), isSigner: false, isWritable: false },
{ pubkey: taker, isSigner: false, isWritable: true },
{ pubkey: maker, isSigner: false, isWritable: true },
{
pubkey: HxroPrintTradeProvider.getLockedCollateralRecordAddress(taker, response.account),
isSigner: false,
isWritable: true,
},
{
pubkey: HxroPrintTradeProvider.getLockedCollateralRecordAddress(maker, response.account),
isSigner: false,
isWritable: true,
},
{ pubkey: HxroPrintTradeProvider.getOperatorAddress(), isSigner: false, isWritable: true },
{ pubkey: HxroPrintTradeProvider.getConfigAddress(), isSigner: false, isWritable: false },
{ pubkey: dexProgram.programId, isSigner: false, isWritable: false },
Expand All @@ -480,8 +496,16 @@ export class HxroPrintTradeProvider {
];
}

getRevertPrintTradeSettlementPreparationAccounts(rfq: Rfq, response: Response) {
return [{ pubkey: this.getProgramId(), isSigner: false, isWritable: false }];
getRevertPrintTradeSettlementPreparationAccounts(rfq: Rfq, response: Response, side: AuthoritySide) {
const lockRecordOwner = side === AuthoritySide.Taker ? this.context.taker.publicKey : this.context.maker.publicKey;
return [
{ pubkey: this.getProgramId(), isSigner: false, isWritable: false },
{
pubkey: HxroPrintTradeProvider.getLockedCollateralRecordAddress(lockRecordOwner, response.account),
isSigner: false,
isWritable: true,
},
];
}

getCleanUpPrintTradeSettlementAccounts(rfq: Rfq, response: Response) {
Expand Down
Loading

0 comments on commit 8dd29a2

Please sign in to comment.