Skip to content

Commit 48af8e8

Browse files
committed
[phantom]: enforce phantom bid window < interval in governance setters
- set_phantom_order_config and set_phantom_bid_window now reject a config where the effective bid window (resolving the 0 fallback to PhantomOrderBidWindowBlocks) is not strictly shorter than interval_blocks. This guarantees the on_finalize invariant: a batch is never replaced on the same block its window closes, so its PhantomBidWindowExhausted snapshot is never dropped. interval_blocks == 0 (generate-once) is exempt as there is no regeneration to race. - New error PhantomBidWindowNotShorterThanInterval. - tests: add PhantomOrderBidWindowBlocks to the mock Config (was missing, which broke the test build) and cover both setters' guard plus the no-config case.
1 parent 4f7cc96 commit 48af8e8

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

modules/pallets/intents-coprocessor/src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ pub mod pallet {
234234
PhantomOrderBidWindowClosed,
235235
/// A filler already has a bid for this phantom order
236236
DuplicatePhantomBid,
237+
/// The effective phantom bid window is not shorter than the generation interval.
238+
/// They must satisfy `window < interval_blocks` so a batch is never replaced on the
239+
/// same block its bid window closes (which would drop its exhaustion snapshot).
240+
PhantomBidWindowNotShorterThanInterval,
237241
}
238242

239243
#[pallet::call]
@@ -546,6 +550,14 @@ pub mod pallet {
546550
let interval_blocks = config.interval_blocks;
547551
let chain = config.chain.clone();
548552

553+
// The bid window must close strictly before the next generation so on_finalize emits
554+
// each batch's exhaustion before on_initialize replaces it. interval_blocks == 0 means
555+
// generate once and never regenerate, so there is no replacement to race.
556+
ensure!(
557+
interval_blocks == 0 || Self::phantom_bid_window() < interval_blocks,
558+
Error::<T>::PhantomBidWindowNotShorterThanInterval
559+
);
560+
549561
PhantomOrderConfig::<T>::put(&config);
550562
CurrentPhantomOrder::<T>::kill();
551563
LastPhantomGeneration::<T>::kill();
@@ -565,6 +577,18 @@ pub mod pallet {
565577
pub fn set_phantom_bid_window(origin: OriginFor<T>, window: u32) -> DispatchResult {
566578
T::GovernanceOrigin::ensure_origin(origin)?;
567579

580+
// Resolve the window the way phantom_bid_window() would: 0 falls back to the
581+
// configured constant. Enforce window < interval_blocks against the active config
582+
// (if any) so a batch is never replaced on the block its bid window closes.
583+
let effective_window =
584+
if window == 0 { T::PhantomOrderBidWindowBlocks::get() } else { window };
585+
if let Some(config) = PhantomOrderConfig::<T>::get() {
586+
ensure!(
587+
config.interval_blocks == 0 || effective_window < config.interval_blocks,
588+
Error::<T>::PhantomBidWindowNotShorterThanInterval
589+
);
590+
}
591+
568592
PhantomBidWindow::<T>::put(window);
569593

570594
Self::deposit_event(Event::PhantomBidWindowUpdated { window });

modules/pallets/intents-coprocessor/src/tests.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use frame_support::{
2525
BoundedVec,
2626
};
2727
use frame_system::EnsureRoot;
28-
use ismp::host::StateMachine;
28+
use ismp::{consensus::StateMachineId, host::StateMachine};
2929
use ismp_testsuite::mocks::MockRouter;
3030

3131
use polkadot_sdk::*;
@@ -136,12 +136,14 @@ impl pallet_ismp::Config for Test {
136136

137137
parameter_types! {
138138
pub const StorageDepositFee: Balance = 100;
139+
pub const PhantomOrderBidWindowBlocks: u32 = 100;
139140
}
140141

141142
impl pallet_intents::Config for Test {
142143
type Dispatcher = Ismp;
143144
type Currency = Balances;
144145
type StorageDepositFee = StorageDepositFee;
146+
type PhantomOrderBidWindowBlocks = PhantomOrderBidWindowBlocks;
145147
type GovernanceOrigin = EnsureRoot<AccountId>;
146148
type WeightInfo = ();
147149
}
@@ -588,3 +590,68 @@ fn multiple_fillers_can_bid_on_same_order() {
588590
assert!(Bids::<Test>::contains_key(&commitment, &filler2));
589591
});
590592
}
593+
594+
fn phantom_config(interval_blocks: u32) -> types::PhantomOrderConfiguration {
595+
types::PhantomOrderConfiguration {
596+
chain: StateMachineId { state_id: StateMachine::Evm(1), consensus_state_id: *b"ETH0" },
597+
token_pairs: BoundedVec::try_from(vec![types::PhantomTokenPair {
598+
token_a: H160::repeat_byte(1),
599+
token_b: H160::repeat_byte(2),
600+
standard_amount: 1_000_000,
601+
}])
602+
.unwrap(),
603+
interval_blocks,
604+
}
605+
}
606+
607+
#[test]
608+
fn set_phantom_order_config_rejects_window_not_shorter_than_interval() {
609+
new_test_ext().execute_with(|| {
610+
// Default PhantomBidWindow is 0, so the effective window is the fallback constant (100).
611+
// An interval equal to or below the window must be rejected.
612+
assert_noop!(
613+
Intents::set_phantom_order_config(RuntimeOrigin::root(), phantom_config(100)),
614+
Error::<Test>::PhantomBidWindowNotShorterThanInterval
615+
);
616+
assert_noop!(
617+
Intents::set_phantom_order_config(RuntimeOrigin::root(), phantom_config(50)),
618+
Error::<Test>::PhantomBidWindowNotShorterThanInterval
619+
);
620+
621+
// An interval strictly greater than the window is accepted.
622+
assert_ok!(Intents::set_phantom_order_config(RuntimeOrigin::root(), phantom_config(200)));
623+
624+
// interval_blocks == 0 means generate-once (no regeneration), so the invariant is vacuous.
625+
assert_ok!(Intents::set_phantom_order_config(RuntimeOrigin::root(), phantom_config(0)));
626+
});
627+
}
628+
629+
#[test]
630+
fn set_phantom_bid_window_rejects_window_not_shorter_than_interval() {
631+
new_test_ext().execute_with(|| {
632+
assert_ok!(Intents::set_phantom_order_config(RuntimeOrigin::root(), phantom_config(200)));
633+
634+
// A window equal to or above the active interval must be rejected.
635+
assert_noop!(
636+
Intents::set_phantom_bid_window(RuntimeOrigin::root(), 200),
637+
Error::<Test>::PhantomBidWindowNotShorterThanInterval
638+
);
639+
assert_noop!(
640+
Intents::set_phantom_bid_window(RuntimeOrigin::root(), 250),
641+
Error::<Test>::PhantomBidWindowNotShorterThanInterval
642+
);
643+
644+
// A window of 0 resolves to the fallback constant (100), which is < 200.
645+
assert_ok!(Intents::set_phantom_bid_window(RuntimeOrigin::root(), 0));
646+
// An explicit window below the interval is accepted.
647+
assert_ok!(Intents::set_phantom_bid_window(RuntimeOrigin::root(), 50));
648+
});
649+
}
650+
651+
#[test]
652+
fn set_phantom_bid_window_allows_any_window_when_no_config() {
653+
new_test_ext().execute_with(|| {
654+
// With no active config there is no interval to violate.
655+
assert_ok!(Intents::set_phantom_bid_window(RuntimeOrigin::root(), 1_000_000));
656+
});
657+
}

0 commit comments

Comments
 (0)