Skip to content

Commit c44105f

Browse files
[2.3] Implement cancel_order() (#49)
* feat(order): implement cancel_order instruction * chore: re-trigger CI
1 parent 5fbb872 commit c44105f

3 files changed

Lines changed: 514 additions & 6 deletions

File tree

src/instruction.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,16 @@ pub struct CancelOrderAccounts<'a> {
215215
pub trader_quote_ata: &'a AccountView,
216216
pub base_vault: &'a AccountView,
217217
pub quote_vault: &'a AccountView,
218+
pub base_mint: &'a AccountView,
219+
pub quote_mint: &'a AccountView,
218220
pub token_program: &'a AccountView,
219221
}
220222

221223
impl<'a> TryFrom<&'a [AccountView]> for CancelOrderAccounts<'a> {
222224
type Error = FluxDexError;
223225

224226
fn try_from(accounts: &'a [AccountView]) -> Result<Self, Self::Error> {
225-
if accounts.len() != 8 {
227+
if accounts.len() != 10 {
226228
return Err(FluxDexError::InvalidAccountCount);
227229
}
228230

@@ -234,6 +236,8 @@ impl<'a> TryFrom<&'a [AccountView]> for CancelOrderAccounts<'a> {
234236
trader_quote_ata,
235237
base_vault,
236238
quote_vault,
239+
base_mint,
240+
quote_mint,
237241
token_program,
238242
] = accounts
239243
else {
@@ -247,14 +251,18 @@ impl<'a> TryFrom<&'a [AccountView]> for CancelOrderAccounts<'a> {
247251

248252
// Writable - all mutable accounts
249253
if !trader.is_writable()
250-
|| !market.is_writable()
251254
|| !order.is_writable()
252255
|| !trader_base_ata.is_writable()
253256
|| !trader_quote_ata.is_writable()
254257
{
255258
return Err(FluxDexError::AccountNotWritable);
256259
}
257260

261+
// Vaults must be writable for the token transfer CPI.
262+
if !base_vault.is_writable() || !quote_vault.is_writable() {
263+
return Err(FluxDexError::AccountNotWritable);
264+
}
265+
258266
// Duplicate mutable account guard
259267
if trader_base_ata.address() == trader_quote_ata.address() {
260268
return Err(FluxDexError::DuplicateMutableAccount);
@@ -273,6 +281,8 @@ impl<'a> TryFrom<&'a [AccountView]> for CancelOrderAccounts<'a> {
273281
trader_quote_ata,
274282
base_vault,
275283
quote_vault,
284+
base_mint,
285+
quote_mint,
276286
token_program,
277287
})
278288
}

src/processor.rs

Lines changed: 242 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const MARKET_ACCOUNT_INDEX: usize = 1;
2222
/// Must match [`PlaceOrderAccounts`] (trader, market, **order**, …).
2323
const ORDER_ACCOUNT_INDEX: usize = 2;
2424

25+
/// Index of the Order account within the `CancelOrder` account list.
26+
/// Must match [`CancelOrderAccounts`] (trader, market, **order**, …).
27+
const CANCEL_ORDER_ACCOUNT_INDEX: usize = 2;
28+
2529
/// Minimum byte length of an SPL Token (or Token-2022 base) `Mint` account.
2630
const MINT_ACCOUNT_LEN: usize = 82;
2731

@@ -524,14 +528,248 @@ fn transfer_checked(
524528
invoke_signed(&ix, &[source, mint, destination, authority], &[])
525529
}
526530

531+
/// Invoke SPL Token `transfer_checked` where the authority is a PDA. The PDA
532+
/// signer seeds are provided so the CPI can be signed on behalf of the PDA.
533+
#[inline]
534+
#[allow(clippy::too_many_arguments)]
535+
fn transfer_checked_pda(
536+
token_program: &Address,
537+
source: &AccountView,
538+
mint: &AccountView,
539+
destination: &AccountView,
540+
authority: &AccountView,
541+
amount: u64,
542+
decimals: u8,
543+
signer: Signer,
544+
) -> ProgramResult {
545+
let mut data = [0u8; 10];
546+
data[0] = TOKEN_IX_TRANSFER_CHECKED;
547+
data[1..9].copy_from_slice(&amount.to_le_bytes());
548+
data[9] = decimals;
549+
550+
let accounts = [
551+
InstructionAccount::new(source.address(), true, false),
552+
InstructionAccount::readonly(mint.address()),
553+
InstructionAccount::new(destination.address(), true, false),
554+
InstructionAccount::new(authority.address(), false, true),
555+
];
556+
let ix = InstructionView {
557+
program_id: token_program,
558+
accounts: &accounts,
559+
data: &data,
560+
};
561+
invoke_signed(&ix, &[source, mint, destination, authority], &[signer])
562+
}
563+
527564
/// Cancel an open order. (TDD §5, ticket 2.3)
528-
/// Account context: CancelOrderAccounts (8 accounts)
565+
/// Account context: CancelOrderAccounts (10 accounts)
529566
/// Args: CancelOrderArgs
567+
///
568+
/// Validates the order is Open and belongs to the signing trader, transfers the
569+
/// locked tokens from the market vault back to the trader's ATA via a
570+
/// Market-PDA-signed `transfer_checked` CPI, and sets the order status to
571+
/// Cancelled.
530572
pub fn cancel_order(
531-
_program_id: &Address,
532-
_accounts: &mut [AccountView],
533-
_instruction_data: &[u8],
573+
program_id: &Address,
574+
accounts: &mut [AccountView],
575+
instruction_data: &[u8],
534576
) -> ProgramResult {
577+
// --- Deserialize args ---
578+
let args = wincode::deserialize::<crate::instruction::CancelOrderArgs>(instruction_data)
579+
.map_err(|_| FluxDexError::InvalidInstructionData)?;
580+
581+
// --- Structural account validation ---
582+
let ctx = crate::instruction::CancelOrderAccounts::try_from(&*accounts)?;
583+
let token_program = ctx.token_program.address();
584+
585+
// --- Market: owner + discriminator, snapshot needed fields ---
586+
if !ctx.market.owned_by(program_id) {
587+
return Err(FluxDexError::InvalidAccountOwner.into());
588+
}
589+
let (m_base_mint, m_quote_mint, m_base_vault, m_quote_vault, m_bump) = {
590+
let data = ctx
591+
.market
592+
.try_borrow()
593+
.map_err(|_| FluxDexError::InvalidInstructionData)?;
594+
let market = Market::from_account(&data)?;
595+
(
596+
market.base_mint,
597+
market.quote_mint,
598+
market.base_vault,
599+
market.quote_vault,
600+
market.bump,
601+
)
602+
};
603+
604+
// --- Verify the market PDA is canonical (re-derive from stored bump) ---
605+
let market_pda = Address::create_program_address(
606+
&[b"market", &m_base_mint, &m_quote_mint, &[m_bump]],
607+
program_id,
608+
)
609+
.map_err(|_| FluxDexError::InvalidPdaSeeds)?;
610+
if &market_pda != ctx.market.address() {
611+
return Err(FluxDexError::InvalidPdaSeeds.into());
612+
}
613+
614+
// --- Bind passed vaults/mints to the market's recorded identities ---
615+
if ctx.base_vault.address().to_bytes() != m_base_vault
616+
|| ctx.quote_vault.address().to_bytes() != m_quote_vault
617+
{
618+
return Err(FluxDexError::InvalidArgument.into());
619+
}
620+
if ctx.base_mint.address().to_bytes() != m_base_mint
621+
|| ctx.quote_mint.address().to_bytes() != m_quote_mint
622+
{
623+
return Err(FluxDexError::InvalidTokenMint.into());
624+
}
625+
626+
// --- Validate mint accounts are token-program-owned ---
627+
validate_mint(ctx.base_mint, token_program)?;
628+
validate_mint(ctx.quote_mint, token_program)?;
629+
630+
// --- Order: owner + discriminator, validate status and trader ---
631+
if !ctx.order.owned_by(program_id) {
632+
return Err(FluxDexError::InvalidAccountOwner.into());
633+
}
634+
let (o_side, o_price, o_quantity, o_filled, o_bump) = {
635+
let data = ctx
636+
.order
637+
.try_borrow()
638+
.map_err(|_| FluxDexError::InvalidInstructionData)?;
639+
let order = Order::from_account(&data)?;
640+
641+
// Only Open orders can be cancelled.
642+
if order.status()? != OrderStatus::Open {
643+
return Err(FluxDexError::OrderNotOpen.into());
644+
}
645+
646+
// Only the order's trader can cancel.
647+
if order.trader != ctx.trader.address().to_bytes() {
648+
return Err(FluxDexError::MissingRequiredSigner.into());
649+
}
650+
651+
// Order must belong to this market.
652+
if order.market != ctx.market.address().to_bytes() {
653+
return Err(FluxDexError::InvalidArgument.into());
654+
}
655+
656+
(
657+
order.side()?,
658+
order.price(),
659+
order.quantity(),
660+
order.filled(),
661+
order.bump,
662+
)
663+
};
664+
665+
// --- Verify the order PDA is canonical ---
666+
// Use create_program_address (O(1)) with stored bump instead of the
667+
// expensive find_program_address (iterates up to 256 bumps, ~6k CU).
668+
let market_bytes = ctx.market.address().to_bytes();
669+
let trader_bytes = ctx.trader.address().to_bytes();
670+
let order_id_bytes = args.order_id.to_le_bytes();
671+
let order_pda = Address::create_program_address(
672+
&[
673+
b"order",
674+
&market_bytes,
675+
&trader_bytes,
676+
&order_id_bytes,
677+
&[o_bump],
678+
],
679+
program_id,
680+
)
681+
.map_err(|_| FluxDexError::InvalidPdaSeeds)?;
682+
if &order_pda != ctx.order.address() {
683+
return Err(FluxDexError::InvalidPdaSeeds.into());
684+
}
685+
686+
// --- Determine refund amount and the custody leg ---
687+
// Refund the unfilled portion: Buy locked price*(quantity-filled) quote,
688+
// Sell locked (quantity-filled) base.
689+
let remaining = o_quantity
690+
.checked_sub(o_filled)
691+
.ok_or(FluxDexError::ArithmeticOverflow)?;
692+
693+
let (source_vault, dest_ata, mint_account, refund_amount) = match o_side {
694+
Side::Buy => (
695+
ctx.quote_vault,
696+
ctx.trader_quote_ata,
697+
ctx.quote_mint,
698+
o_price
699+
.checked_mul(remaining)
700+
.ok_or(FluxDexError::ArithmeticOverflow)?,
701+
),
702+
Side::Sell => (
703+
ctx.base_vault,
704+
ctx.trader_base_ata,
705+
ctx.base_mint,
706+
remaining,
707+
),
708+
};
709+
710+
// --- Read mint decimals for transfer_checked ---
711+
let decimals = read_mint_decimals(mint_account)?;
712+
713+
// --- Snapshot the vault balance before the CPI ---
714+
let vault_balance_before = {
715+
let data = source_vault
716+
.try_borrow()
717+
.map_err(|_| FluxDexError::InvalidInstructionData)?;
718+
data.get(TOKEN_AMOUNT_OFFSET..TOKEN_AMOUNT_OFFSET + 8)
719+
.and_then(|b| b.try_into().ok())
720+
.map(u64::from_le_bytes)
721+
.ok_or(FluxDexError::InvalidInstructionData)?
722+
};
723+
724+
// --- CPI: transfer_checked from vault → trader ATA (Market PDA signs) ---
725+
let market_bump_seed = [m_bump];
726+
let market_seeds = [
727+
Seed::from(b"market".as_ref()),
728+
Seed::from(m_base_mint.as_ref()),
729+
Seed::from(m_quote_mint.as_ref()),
730+
Seed::from(market_bump_seed.as_ref()),
731+
];
732+
let market_signer = Signer::from(&market_seeds[..]);
733+
734+
transfer_checked_pda(
735+
token_program,
736+
source_vault,
737+
mint_account,
738+
dest_ata,
739+
ctx.market,
740+
refund_amount,
741+
decimals,
742+
market_signer,
743+
)?;
744+
745+
// --- Re-check vault balance after CPI ---
746+
let vault_balance_after = {
747+
let data = source_vault
748+
.try_borrow()
749+
.map_err(|_| FluxDexError::InvalidInstructionData)?;
750+
data.get(TOKEN_AMOUNT_OFFSET..TOKEN_AMOUNT_OFFSET + 8)
751+
.and_then(|b| b.try_into().ok())
752+
.map(u64::from_le_bytes)
753+
.ok_or(FluxDexError::InvalidInstructionData)?
754+
};
755+
if vault_balance_before
756+
.checked_sub(vault_balance_after)
757+
.ok_or(FluxDexError::ArithmeticOverflow)?
758+
!= refund_amount
759+
{
760+
return Err(FluxDexError::AtomicSettlementFailed.into());
761+
}
762+
763+
// --- Update the order status to Cancelled ---
764+
{
765+
let order_account = &mut accounts[CANCEL_ORDER_ACCOUNT_INDEX];
766+
let mut order_data = order_account
767+
.try_borrow_mut()
768+
.map_err(|_| FluxDexError::InvalidInstructionData)?;
769+
let order = Order::from_account_mut(&mut order_data)?;
770+
order.status = u8::from(OrderStatus::Cancelled);
771+
}
772+
535773
Ok(())
536774
}
537775

0 commit comments

Comments
 (0)