@@ -22,6 +22,10 @@ const MARKET_ACCOUNT_INDEX: usize = 1;
2222/// Must match [`PlaceOrderAccounts`] (trader, market, **order**, …).
2323const 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.
2630const 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.
530572pub 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