@@ -43,20 +43,20 @@ const MARKET_USER_SEED: &[u8] = b"market_user";
4343// `#[account(zero)]` check fails if the account size is wrong.
4444const ORDER_BOOK_ACCOUNT_SIZE : u64 = order_book:: state:: ORDER_BOOK_ACCOUNT_SIZE as u64 ;
4545
46- // NVDAx has 8 decimals on-chain; USDC has 6. Because the program stores
47- // price as raw_quote per raw_base, one tick = 10^(base_dec - quote_dec) = 100
48- // USDC/share — the minimum representable price step with these two mints.
46+ // NVDAx has 8 decimals on-chain; USDC has 6.
4947const BASE_DECIMALS : u8 = 8 ; // NVDAx (https://explorer.solana.com/address/Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh)
5048const QUOTE_DECIMALS : u8 = 6 ; // USDC
5149
52- // Market parameters used across every test. `tick_size = 1` and
53- // `base_lot_size = 100` match the NVDAx/USDC decimal configuration
54- // (BASE_DECIMALS=8, QUOTE_DECIMALS=6): price = human USDC/share,
55- // tick = $1.00, 1 lot = 100 raw NVDAx. A dedicated test overrides
56- // tick_size to verify the tick check fires.
50+ // Two-lot model for NVDAx/USDC (d_base=8, d_quote=6):
51+ // base_lot_size = 10^max(8-6, 0) = 100 → 1 lot = 100 raw NVDAx
52+ // quote_lot_size = 10^max(6-8, 0) = 1 → 1 quote-lot = 1 raw USDC
53+ // raw_base = quantity × 100
54+ // raw_quote = price × quantity × 1 (= human USDC/share × lots)
55+ // tick_size = 1 → $1.00 minimum price increment
5756const FEE_BASIS_POINTS : u16 = 10 ;
5857const TICK_SIZE : u64 = 1 ;
5958const BASE_LOT_SIZE : u64 = 100 ;
59+ const QUOTE_LOT_SIZE : u64 = 1 ;
6060const MIN_ORDER_SIZE : u64 = 1 ;
6161
6262// Funding for each trader's token accounts. Large enough to cover every
@@ -254,6 +254,7 @@ fn build_initialize_market_ix(
254254 fee_basis_points : u16 ,
255255 tick_size : u64 ,
256256 base_lot_size : u64 ,
257+ quote_lot_size : u64 ,
257258 min_order_size : u64 ,
258259) -> Instruction {
259260 Instruction :: new_with_bytes (
@@ -262,6 +263,7 @@ fn build_initialize_market_ix(
262263 fee_basis_points,
263264 tick_size,
264265 base_lot_size,
266+ quote_lot_size,
265267 min_order_size,
266268 }
267269 . data ( ) ,
@@ -454,7 +456,7 @@ fn initialize_market_and_users(sc: &mut Scenario) {
454456 // program, zero-initialized) before initialize_market's `#[account(zero)]`
455457 // check passes.
456458 let create_ix = build_create_order_book_account_ix ( sc, & sc. authority . pubkey ( ) ) ;
457- let init_ix = build_initialize_market_ix ( sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , MIN_ORDER_SIZE ) ;
459+ let init_ix = build_initialize_market_ix ( sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , QUOTE_LOT_SIZE , MIN_ORDER_SIZE ) ;
458460 send_transaction_from_instructions (
459461 & mut sc. svm ,
460462 vec ! [ create_ix, init_ix] ,
@@ -497,7 +499,7 @@ fn initialize_market_sets_market_and_order_book() {
497499 let mut sc = full_setup ( ) ;
498500
499501 let create_ix = build_create_order_book_account_ix ( & sc, & sc. authority . pubkey ( ) ) ;
500- let ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , MIN_ORDER_SIZE ) ;
502+ let ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , QUOTE_LOT_SIZE , MIN_ORDER_SIZE ) ;
501503 send_transaction_from_instructions (
502504 & mut sc. svm ,
503505 vec ! [ create_ix, ix] ,
@@ -545,7 +547,7 @@ fn create_market_user_tracks_market_and_owner() {
545547 let mut sc = full_setup ( ) ;
546548
547549 let create_ix = build_create_order_book_account_ix ( & sc, & sc. authority . pubkey ( ) ) ;
548- let init_ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , MIN_ORDER_SIZE ) ;
550+ let init_ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , QUOTE_LOT_SIZE , MIN_ORDER_SIZE ) ;
549551 send_transaction_from_instructions (
550552 & mut sc. svm ,
551553 vec ! [ create_ix, init_ix] ,
@@ -597,8 +599,8 @@ fn place_bid_locks_quote_in_vault() {
597599 send_transaction_from_instructions ( & mut sc. svm , vec ! [ ix] , & [ & sc. buyer ] , & sc. buyer . pubkey ( ) )
598600 . unwrap ( ) ;
599601
600- // A bid locks price * quantity in the quote vault .
601- let locked_quote = BID_PRICE * BID_QUANTITY ;
602+ // A bid locks price * quantity * quote_lot_size raw quote tokens .
603+ let locked_quote = BID_PRICE * BID_QUANTITY * QUOTE_LOT_SIZE ;
602604 assert_eq ! (
603605 get_token_account_balance( & sc. svm, & sc. quote_vault. pubkey( ) ) . unwrap( ) ,
604606 locked_quote
@@ -693,7 +695,7 @@ fn place_order_rejects_unaligned_tick() {
693695 let unusual_tick_size: u64 = 50 ;
694696 let create_ix = build_create_order_book_account_ix ( & sc, & sc. authority . pubkey ( ) ) ;
695697 let init_ix =
696- build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , unusual_tick_size, BASE_LOT_SIZE , MIN_ORDER_SIZE ) ;
698+ build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , unusual_tick_size, BASE_LOT_SIZE , QUOTE_LOT_SIZE , MIN_ORDER_SIZE ) ;
697699 send_transaction_from_instructions (
698700 & mut sc. svm ,
699701 vec ! [ create_ix, init_ix] ,
@@ -750,7 +752,7 @@ fn place_order_rejects_below_min_order_size() {
750752 let elevated_min_order_size: u64 = 10 ;
751753 let create_ix = build_create_order_book_account_ix ( & sc, & sc. authority . pubkey ( ) ) ;
752754 let init_ix =
753- build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , elevated_min_order_size) ;
755+ build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , QUOTE_LOT_SIZE , elevated_min_order_size) ;
754756 send_transaction_from_instructions (
755757 & mut sc. svm ,
756758 vec ! [ create_ix, init_ix] ,
@@ -1087,7 +1089,7 @@ fn initialize_market_rejects_zero_tick_size() {
10871089
10881090 let zero_tick_size: u64 = 0 ;
10891091 let create_ix = build_create_order_book_account_ix ( & sc, & sc. authority . pubkey ( ) ) ;
1090- let ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , zero_tick_size, BASE_LOT_SIZE , MIN_ORDER_SIZE ) ;
1092+ let ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , zero_tick_size, BASE_LOT_SIZE , QUOTE_LOT_SIZE , MIN_ORDER_SIZE ) ;
10911093 let result = send_transaction_from_instructions (
10921094 & mut sc. svm ,
10931095 vec ! [ create_ix, ix] ,
@@ -1108,7 +1110,7 @@ fn initialize_market_rejects_zero_base_lot_size() {
11081110 let mut sc = full_setup ( ) ;
11091111
11101112 let create_ix = build_create_order_book_account_ix ( & sc, & sc. authority . pubkey ( ) ) ;
1111- let ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , 0 , MIN_ORDER_SIZE ) ;
1113+ let ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , 0 , QUOTE_LOT_SIZE , MIN_ORDER_SIZE ) ;
11121114 let result = send_transaction_from_instructions (
11131115 & mut sc. svm ,
11141116 vec ! [ create_ix, ix] ,
@@ -1124,6 +1126,27 @@ fn initialize_market_rejects_zero_base_lot_size() {
11241126 assert ! ( result. is_err( ) , "base_lot_size == 0 must be rejected" ) ;
11251127}
11261128
1129+ #[ test]
1130+ fn initialize_market_rejects_zero_quote_lot_size ( ) {
1131+ let mut sc = full_setup ( ) ;
1132+
1133+ let create_ix = build_create_order_book_account_ix ( & sc, & sc. authority . pubkey ( ) ) ;
1134+ let ix = build_initialize_market_ix ( & sc, FEE_BASIS_POINTS , TICK_SIZE , BASE_LOT_SIZE , 0 , MIN_ORDER_SIZE ) ;
1135+ let result = send_transaction_from_instructions (
1136+ & mut sc. svm ,
1137+ vec ! [ create_ix, ix] ,
1138+ & [
1139+ & sc. authority ,
1140+ & sc. order_book ,
1141+ & sc. base_vault ,
1142+ & sc. quote_vault ,
1143+ & sc. fee_vault ,
1144+ ] ,
1145+ & sc. authority . pubkey ( ) ,
1146+ ) ;
1147+ assert ! ( result. is_err( ) , "quote_lot_size == 0 must be rejected" ) ;
1148+ }
1149+
11271150#[ test]
11281151fn initialize_market_rejects_oversized_fee ( ) {
11291152 let mut sc = full_setup ( ) ;
@@ -1136,6 +1159,7 @@ fn initialize_market_rejects_oversized_fee() {
11361159 over_cap_fee_basis_points,
11371160 TICK_SIZE ,
11381161 BASE_LOT_SIZE ,
1162+ QUOTE_LOT_SIZE ,
11391163 MIN_ORDER_SIZE ,
11401164 ) ;
11411165 let result = send_transaction_from_instructions (
@@ -1238,7 +1262,7 @@ fn taker_bid_fully_crosses_best_ask() {
12381262 // that trader starting balances easily cover it.
12391263 const PRICE : u64 = 1000 ;
12401264 const QUANTITY : u64 = 100 ;
1241- const EXPECTED_GROSS_QUOTE : u64 = PRICE * QUANTITY ;
1265+ const EXPECTED_GROSS_QUOTE : u64 = PRICE * QUANTITY * QUOTE_LOT_SIZE ;
12421266 const EXPECTED_FEE : u64 = EXPECTED_GROSS_QUOTE * FEE_BASIS_POINTS as u64 / 10_000 ;
12431267 const EXPECTED_NET_TO_MAKER : u64 = EXPECTED_GROSS_QUOTE - EXPECTED_FEE ;
12441268
@@ -1316,7 +1340,7 @@ fn taker_ask_fully_crosses_best_bid() {
13161340 const MAKER_BID_ID : u64 = 1 ;
13171341 const PRICE : u64 = 1000 ;
13181342 const QUANTITY : u64 = 100 ;
1319- const EXPECTED_GROSS_QUOTE : u64 = PRICE * QUANTITY ;
1343+ const EXPECTED_GROSS_QUOTE : u64 = PRICE * QUANTITY * QUOTE_LOT_SIZE ;
13201344 const EXPECTED_FEE : u64 = EXPECTED_GROSS_QUOTE * FEE_BASIS_POINTS as u64 / 10_000 ;
13211345 const EXPECTED_NET_TO_TAKER : u64 = EXPECTED_GROSS_QUOTE - EXPECTED_FEE ;
13221346
@@ -1618,14 +1642,13 @@ fn taker_crosses_multiple_resting_orders_best_price_first() {
16181642 assert_eq ! ( buyer_base, TAKER_BID_QUANTITY * BASE_LOT_SIZE ) ;
16191643
16201644 // Price-improvement rebate: taker locked at 1000/unit but 30 units
1621- // filled at 900. Rebate = (1000 - 900) * 30 = 3_000 .
1622- const PRICE_IMPROVEMENT_REBATE : u64 = ( TAKER_BID_PRICE - BEST_ASK_PRICE ) * BEST_ASK_QUANTITY ;
1645+ // filled at 900. Rebate = (1000 - 900) * 30 * quote_lot_size .
1646+ const PRICE_IMPROVEMENT_REBATE : u64 = ( TAKER_BID_PRICE - BEST_ASK_PRICE ) * BEST_ASK_QUANTITY * QUOTE_LOT_SIZE ;
16231647 assert_eq ! ( buyer_quote_rebate, PRICE_IMPROVEMENT_REBATE ) ;
16241648
1625- // Seller's net unsettled_quote = sum of (fill_price * fill_qty - fee)
1626- // across both fills.
1627- let gross_one: u64 = BEST_ASK_PRICE * BEST_ASK_QUANTITY ;
1628- let gross_two: u64 = SECOND_ASK_PRICE * SECOND_ASK_QUANTITY ;
1649+ // Seller's net unsettled_quote = sum of (fill_price * fill_qty * quote_lot_size - fee).
1650+ let gross_one: u64 = BEST_ASK_PRICE * BEST_ASK_QUANTITY * QUOTE_LOT_SIZE ;
1651+ let gross_two: u64 = SECOND_ASK_PRICE * SECOND_ASK_QUANTITY * QUOTE_LOT_SIZE ;
16291652 let fee_one: u64 = gross_one * FEE_BASIS_POINTS as u64 / 10_000 ;
16301653 let fee_two: u64 = gross_two * FEE_BASIS_POINTS as u64 / 10_000 ;
16311654 let expected_seller_quote = ( gross_one - fee_one) + ( gross_two - fee_two) ;
@@ -1783,16 +1806,15 @@ fn taker_bid_gets_price_improvement_from_resting_ask() {
17831806 . unwrap ( ) ;
17841807
17851808 // Maker got 900-per-unit (minus fee), not 1000.
1786- let gross_to_maker: u64 = MAKER_ASK_PRICE * QUANTITY ;
1809+ let gross_to_maker: u64 = MAKER_ASK_PRICE * QUANTITY * QUOTE_LOT_SIZE ;
17871810 let fee: u64 = gross_to_maker * FEE_BASIS_POINTS as u64 / 10_000 ;
17881811 let expected_net_to_maker: u64 = gross_to_maker - fee;
17891812 let ( _, seller_quote) = read_user_unsettled ( & sc. svm , & sc. seller_market_user ) ;
17901813 assert_eq ! ( seller_quote, expected_net_to_maker) ;
17911814
1792- // Taker locked (TAKER_BID_PRICE * QUANTITY) of quote up front; only
1793- // (MAKER_ASK_PRICE * QUANTITY) was spent. The difference is the
1794- // price-improvement rebate.
1795- let expected_rebate: u64 = ( TAKER_BID_PRICE - MAKER_ASK_PRICE ) * QUANTITY ;
1815+ // Taker locked (TAKER_BID_PRICE * QUANTITY * QUOTE_LOT_SIZE) up front;
1816+ // only (MAKER_ASK_PRICE * QUANTITY * QUOTE_LOT_SIZE) was spent.
1817+ let expected_rebate: u64 = ( TAKER_BID_PRICE - MAKER_ASK_PRICE ) * QUANTITY * QUOTE_LOT_SIZE ;
17961818 let ( buyer_base, buyer_quote) = read_user_unsettled ( & sc. svm , & sc. buyer_market_user ) ;
17971819 assert_eq ! ( buyer_base, QUANTITY * BASE_LOT_SIZE ) ;
17981820 assert_eq ! ( buyer_quote, expected_rebate) ;
@@ -1808,7 +1830,7 @@ fn fee_vault_receives_exactly_bps_of_taker_gross() {
18081830 const MAKER_ASK_ID : u64 = 1 ;
18091831 const PRICE : u64 = 500 ;
18101832 const QUANTITY : u64 = 200 ;
1811- const GROSS : u64 = PRICE * QUANTITY ;
1833+ const GROSS : u64 = PRICE * QUANTITY * QUOTE_LOT_SIZE ;
18121834 const EXPECTED_FEE : u64 = GROSS * FEE_BASIS_POINTS as u64 / 10_000 ;
18131835
18141836 let __ix5 = build_place_order_ix (
@@ -1865,7 +1887,7 @@ fn authority_can_withdraw_fees_after_match() {
18651887 const MAKER_ASK_ID : u64 = 1 ;
18661888 const PRICE : u64 = 2000 ;
18671889 const QUANTITY : u64 = 50 ;
1868- const GROSS : u64 = PRICE * QUANTITY ;
1890+ const GROSS : u64 = PRICE * QUANTITY * QUOTE_LOT_SIZE ;
18691891 const EXPECTED_FEE : u64 = GROSS * FEE_BASIS_POINTS as u64 / 10_000 ;
18701892
18711893 let __ix7 = build_place_order_ix (
@@ -1934,7 +1956,7 @@ fn settle_funds_after_match_pays_out_both_unsettled_balances() {
19341956 const MAKER_ASK_ID : u64 = 1 ;
19351957 const PRICE : u64 = 1000 ;
19361958 const QUANTITY : u64 = 100 ;
1937- const GROSS : u64 = PRICE * QUANTITY ;
1959+ const GROSS : u64 = PRICE * QUANTITY * QUOTE_LOT_SIZE ;
19381960 const EXPECTED_FEE : u64 = GROSS * FEE_BASIS_POINTS as u64 / 10_000 ;
19391961 const EXPECTED_NET_QUOTE_TO_SELLER : u64 = GROSS - EXPECTED_FEE ;
19401962
0 commit comments