@@ -171,6 +171,7 @@ fn build_create_lease_ix(
171171 duration_seconds : i64 ,
172172 maintenance_margin_bps : u16 ,
173173 liquidation_bounty_bps : u16 ,
174+ feed_id : [ u8 ; 32 ] ,
174175) -> Instruction {
175176 let ( lease, leased_vault, collateral_vault) =
176177 lease_pdas ( & sc. program_id , & sc. lessor . pubkey ( ) , lease_id) ;
@@ -184,6 +185,7 @@ fn build_create_lease_ix(
184185 duration_seconds,
185186 maintenance_margin_bps,
186187 liquidation_bounty_bps,
188+ feed_id,
187189 }
188190 . data ( ) ,
189191 asset_leasing:: accounts:: CreateLease {
@@ -349,7 +351,12 @@ fn build_close_expired_ix(sc: &Scenario, lease_id: u64) -> Instruction {
349351/// Build a minimal `PriceUpdateV2` account body with the requested price and
350352/// exponent, timestamped `publish_time`. Fields not used by the program are
351353/// filled with zero bytes.
352- fn build_price_update_data ( price : i64 , exponent : i32 , publish_time : i64 ) -> Vec < u8 > {
354+ fn build_price_update_data (
355+ feed_id : [ u8 ; 32 ] ,
356+ price : i64 ,
357+ exponent : i32 ,
358+ publish_time : i64 ,
359+ ) -> Vec < u8 > {
353360 // Size layout:
354361 // 8 disc + 32 write_authority + 1 verification_level + 32 feed_id +
355362 // 8 price + 8 conf + 4 exponent + 8 publish_time + 8 prev_publish_time +
@@ -361,8 +368,7 @@ fn build_price_update_data(price: i64, exponent: i32, publish_time: i64) -> Vec<
361368 data. extend_from_slice ( & [ 0u8 ; 32 ] ) ;
362369 // verification_level = Full (1).
363370 data. push ( 1 ) ;
364- // feed_id — arbitrary; not checked by the program.
365- data. extend_from_slice ( & [ 0xAB ; 32 ] ) ;
371+ data. extend_from_slice ( & feed_id) ;
366372 data. extend_from_slice ( & price. to_le_bytes ( ) ) ;
367373 data. extend_from_slice ( & 0u64 . to_le_bytes ( ) ) ; // conf
368374 data. extend_from_slice ( & exponent. to_le_bytes ( ) ) ;
@@ -378,11 +384,12 @@ fn build_price_update_data(price: i64, exponent: i32, publish_time: i64) -> Vec<
378384fn mock_price_update (
379385 svm : & mut LiteSVM ,
380386 address : Pubkey ,
387+ feed_id : [ u8 ; 32 ] ,
381388 price : i64 ,
382389 exponent : i32 ,
383390 publish_time : i64 ,
384391) {
385- let data = build_price_update_data ( price, exponent, publish_time) ;
392+ let data = build_price_update_data ( feed_id , price, exponent, publish_time) ;
386393 let lamports = svm. minimum_balance_for_rent_exemption ( data. len ( ) ) ;
387394 let owner: Pubkey = PYTH_RECEIVER_PROGRAM_ID_STR . parse ( ) . unwrap ( ) ;
388395 svm. set_account (
@@ -409,6 +416,11 @@ const RENT_PER_SECOND: u64 = 10; // 10 base-units / sec
409416const DURATION_SECONDS : i64 = 60 * 60 * 24 ; // 24h
410417const MAINTENANCE_MARGIN_BPS : u16 = 12_000 ; // 120%
411418const LIQUIDATION_BOUNTY_BPS : u16 = 500 ; // 5%
419+ // Arbitrary 32-byte Pyth feed id the tests pin their leases to. The
420+ // mocked `PriceUpdateV2` accounts carry the same id so the feed-pinning
421+ // check in liquidate passes. `liquidate_rejects_mismatched_price_feed`
422+ // flips one byte of this to prove the check rejects foreign feeds.
423+ const FEED_ID : [ u8 ; 32 ] = [ 0xAB ; 32 ] ;
412424
413425#[ test]
414426fn create_lease_locks_tokens_and_lists ( ) {
@@ -424,6 +436,7 @@ fn create_lease_locks_tokens_and_lists() {
424436 DURATION_SECONDS ,
425437 MAINTENANCE_MARGIN_BPS ,
426438 LIQUIDATION_BOUNTY_BPS ,
439+ FEED_ID ,
427440 ) ;
428441 send_transaction_from_instructions ( & mut sc. svm , vec ! [ ix] , & [ & sc. lessor ] , & sc. lessor . pubkey ( ) )
429442 . unwrap ( ) ;
@@ -467,6 +480,7 @@ fn take_lease_posts_collateral_and_delivers_tokens() {
467480 DURATION_SECONDS ,
468481 MAINTENANCE_MARGIN_BPS ,
469482 LIQUIDATION_BOUNTY_BPS ,
483+ FEED_ID ,
470484 ) ;
471485 send_transaction_from_instructions (
472486 & mut sc. svm ,
@@ -520,6 +534,7 @@ fn pay_rent_streams_collateral_by_elapsed_time() {
520534 DURATION_SECONDS ,
521535 MAINTENANCE_MARGIN_BPS ,
522536 LIQUIDATION_BOUNTY_BPS ,
537+ FEED_ID ,
523538 ) ;
524539 let take_ix = build_take_lease_ix ( & sc, lease_id) ;
525540 send_transaction_from_instructions (
@@ -569,6 +584,7 @@ fn top_up_collateral_increases_vault_balance() {
569584 DURATION_SECONDS ,
570585 MAINTENANCE_MARGIN_BPS ,
571586 LIQUIDATION_BOUNTY_BPS ,
587+ FEED_ID ,
572588 ) ;
573589 let take_ix = build_take_lease_ix ( & sc, lease_id) ;
574590 send_transaction_from_instructions (
@@ -610,6 +626,7 @@ fn return_lease_refunds_unused_collateral() {
610626 DURATION_SECONDS ,
611627 MAINTENANCE_MARGIN_BPS ,
612628 LIQUIDATION_BOUNTY_BPS ,
629+ FEED_ID ,
613630 ) ;
614631 let take_ix = build_take_lease_ix ( & sc, lease_id) ;
615632 send_transaction_from_instructions (
@@ -675,6 +692,7 @@ fn liquidate_seizes_collateral_on_price_drop() {
675692 DURATION_SECONDS ,
676693 MAINTENANCE_MARGIN_BPS ,
677694 LIQUIDATION_BOUNTY_BPS ,
695+ FEED_ID ,
678696 ) ;
679697 let take_ix = build_take_lease_ix ( & sc, lease_id) ;
680698 send_transaction_from_instructions (
@@ -698,6 +716,7 @@ fn liquidate_seizes_collateral_on_price_drop() {
698716 mock_price_update (
699717 & mut sc. svm ,
700718 price_update_key. pubkey ( ) ,
719+ FEED_ID ,
701720 4 ,
702721 0 ,
703722 now, // fresh publish_time
@@ -751,6 +770,7 @@ fn liquidate_rejects_healthy_position() {
751770 DURATION_SECONDS ,
752771 MAINTENANCE_MARGIN_BPS ,
753772 LIQUIDATION_BOUNTY_BPS ,
773+ FEED_ID ,
754774 ) ;
755775 let take_ix = build_take_lease_ix ( & sc, lease_id) ;
756776 send_transaction_from_instructions (
@@ -766,7 +786,7 @@ fn liquidate_rejects_healthy_position() {
766786 // to fail with `PositionHealthy`.
767787 let price_update_key = Keypair :: new ( ) ;
768788 let now = current_clock ( & sc. svm ) ;
769- mock_price_update ( & mut sc. svm , price_update_key. pubkey ( ) , 1 , 0 , now) ;
789+ mock_price_update ( & mut sc. svm , price_update_key. pubkey ( ) , FEED_ID , 1 , 0 , now) ;
770790
771791 let liq_ix = build_liquidate_ix ( & sc, lease_id, price_update_key. pubkey ( ) ) ;
772792 let result = send_transaction_from_instructions (
@@ -778,6 +798,68 @@ fn liquidate_rejects_healthy_position() {
778798 assert ! ( result. is_err( ) , "healthy liquidation must fail" ) ;
779799}
780800
801+ #[ test]
802+ fn liquidate_rejects_mismatched_price_feed ( ) {
803+ // The lessor pinned FEED_ID; we hand the handler a price update whose
804+ // internal feed_id is different. Even when the price would push the
805+ // position underwater, the liquidate call must bail with
806+ // `PriceFeedMismatch` before running the undercollateralisation check.
807+ let mut sc = full_setup ( ) ;
808+ let lease_id = 100u64 ;
809+
810+ let create_ix = build_create_lease_ix (
811+ & sc,
812+ lease_id,
813+ LEASED_AMOUNT ,
814+ REQUIRED_COLLATERAL ,
815+ RENT_PER_SECOND ,
816+ DURATION_SECONDS ,
817+ MAINTENANCE_MARGIN_BPS ,
818+ LIQUIDATION_BOUNTY_BPS ,
819+ FEED_ID ,
820+ ) ;
821+ let take_ix = build_take_lease_ix ( & sc, lease_id) ;
822+ send_transaction_from_instructions (
823+ & mut sc. svm ,
824+ vec ! [ create_ix, take_ix] ,
825+ & [ & sc. lessor , & sc. lessee ] ,
826+ & sc. lessor . pubkey ( ) ,
827+ )
828+ . unwrap ( ) ;
829+
830+ // Flip every byte — any 32-byte feed id other than FEED_ID should do.
831+ let wrong_feed_id = [ 0xCD ; 32 ] ;
832+
833+ // Price that *would* trigger liquidation (debt 400 vs 200 collateral,
834+ // same as `liquidate_seizes_collateral_on_price_drop`) — except this
835+ // update carries the wrong feed id.
836+ let price_update_key = Keypair :: new ( ) ;
837+ let now = current_clock ( & sc. svm ) ;
838+ mock_price_update (
839+ & mut sc. svm ,
840+ price_update_key. pubkey ( ) ,
841+ wrong_feed_id,
842+ 4 ,
843+ 0 ,
844+ now,
845+ ) ;
846+
847+ let liq_ix = build_liquidate_ix ( & sc, lease_id, price_update_key. pubkey ( ) ) ;
848+ let result = send_transaction_from_instructions (
849+ & mut sc. svm ,
850+ vec ! [ liq_ix] ,
851+ & [ & sc. keeper ] ,
852+ & sc. keeper . pubkey ( ) ,
853+ ) ;
854+ let err = result. expect_err ( "liquidate must reject foreign price feeds" ) ;
855+ let rendered = format ! ( "{err:?}" ) ;
856+ // PriceFeedMismatch is the 16th error in the enum (index 15) → 0x177f.
857+ assert ! (
858+ rendered. contains( "PriceFeedMismatch" ) || rendered. contains( "0x177f" ) ,
859+ "unexpected failure mode: {rendered}"
860+ ) ;
861+ }
862+
781863#[ test]
782864fn close_expired_reclaims_collateral_after_end_ts ( ) {
783865 let mut sc = full_setup ( ) ;
@@ -792,6 +874,7 @@ fn close_expired_reclaims_collateral_after_end_ts() {
792874 DURATION_SECONDS ,
793875 MAINTENANCE_MARGIN_BPS ,
794876 LIQUIDATION_BOUNTY_BPS ,
877+ FEED_ID ,
795878 ) ;
796879 let take_ix = build_take_lease_ix ( & sc, lease_id) ;
797880 send_transaction_from_instructions (
@@ -848,6 +931,7 @@ fn close_expired_cancels_listed_lease() {
848931 DURATION_SECONDS ,
849932 MAINTENANCE_MARGIN_BPS ,
850933 LIQUIDATION_BOUNTY_BPS ,
934+ FEED_ID ,
851935 ) ;
852936 send_transaction_from_instructions (
853937 & mut sc. svm ,
@@ -904,6 +988,7 @@ fn create_lease_rejects_same_mint_for_leased_and_collateral() {
904988 duration_seconds : DURATION_SECONDS ,
905989 maintenance_margin_bps : MAINTENANCE_MARGIN_BPS ,
906990 liquidation_bounty_bps : LIQUIDATION_BOUNTY_BPS ,
991+ feed_id : FEED_ID ,
907992 }
908993 . data ( ) ,
909994 asset_leasing:: accounts:: CreateLease {
0 commit comments