Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions modules/pallets/intents-coprocessor/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,44 @@ mod benchmarks {
Ok(())
}

#[benchmark]
fn set_phantom_order_config() -> Result<(), BenchmarkError> {
let origin =
T::GovernanceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let config = types::PhantomOrderConfiguration {
chain: StateMachine::Evm(8453),
token_pairs: vec![types::PhantomTokenPair {
token_a: H160::repeat_byte(0x01),
token_b: H160::repeat_byte(0x02),
standard_amount: 1_000_000_000_000_000_000u128,
min_output: 990_000_000_000_000_000u128,
}]
.try_into()
.expect("one pair fits in BoundedVec<_, 10>"),
interval_blocks: 10,
};

#[extrinsic_call]
_(origin as T::RuntimeOrigin, config);

assert!(PhantomOrderConfig::<T>::get().is_some());
Ok(())
}

#[benchmark]
fn set_phantom_bid_window() -> Result<(), BenchmarkError> {
let origin =
T::GovernanceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let window: u32 = 100;

#[extrinsic_call]
_(origin as T::RuntimeOrigin, window);

// Verify the window was stored
assert_eq!(PhantomBidWindow::<T>::get(), window);

Ok(())
}

impl_benchmark_test_suite!(Pallet, crate::tests::new_test_ext(), crate::tests::Test);
}
182 changes: 176 additions & 6 deletions modules/pallets/intents-coprocessor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ use polkadot_sdk::*;
use primitive_types::{H160, H256};
use sp_core::Get;
use sp_io::offchain_index;
use sp_runtime::traits::{ConstU32, Zero};
use sp_runtime::{
traits::{ConstU32, Zero},
SaturatedConversion,
};
pub use weights::WeightInfo;

use types::{Bid, GatewayInfo, IntentGatewayParams, RequestKind, TokenDecimalsUpdate, TokenInfo};
use types::{
Bid, GatewayInfo, IntentGatewayParams, PhantomOrderConfiguration, PhantomOrderInfo,
PhantomTokenPair, RequestKind, TokenDecimalsUpdate, TokenInfo,
};

// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;
Expand All @@ -64,6 +70,7 @@ pub mod pallet {
use crate::alloc::string::ToString;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use polkadot_sdk::sp_runtime::traits::Saturating;

#[pallet::pallet]
#[pallet::without_storage_info]
Expand All @@ -83,6 +90,11 @@ pub mod pallet {
#[pallet::constant]
type StorageDepositFee: Get<BalanceOf<Self>>;

/// How many blocks after phantom order creation bids are accepted. Fallback when
/// the PhantomBidWindow storage value is zero.
#[pallet::constant]
type PhantomOrderBidWindowBlocks: Get<u32>;

/// Origin that can perform governance actions
type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;

Expand Down Expand Up @@ -119,6 +131,23 @@ pub mod pallet {
pub type Gateways<T: Config> =
StorageMap<_, Blake2_128Concat, StateMachine, GatewayInfo, OptionQuery>;

/// The single active phantom order. Only one is recognised at a time; the hook
/// replaces it on each generation cycle.
#[pallet::storage]
pub type CurrentPhantomOrder<T: Config> =
StorageValue<_, (H256, PhantomOrderInfo<BlockNumberFor<T>>), OptionQuery>;

/// Governance-updatable bid acceptance window for phantom orders (in blocks).
/// Falls back to PhantomOrderBidWindowBlocks when zero.
#[pallet::storage]
pub type PhantomBidWindow<T: Config> = StorageValue<_, u32, ValueQuery>;

/// Governance-settable phantom order configuration. When present, the
/// on_initialize hook generates a new phantom commitment every interval_blocks.
#[pallet::storage]
pub type PhantomOrderConfig<T: Config> =
StorageValue<_, PhantomOrderConfiguration, OptionQuery>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Expand Down Expand Up @@ -147,6 +176,20 @@ pub mod pallet {
},
/// Storage deposit fee was updated
StorageDepositFeeUpdated { fee: BalanceOf<T> },
/// The runtime generated a new phantom order commitment
PhantomOrderRegistered {
commitment: H256,
chain: Vec<u8>,
created_at: BlockNumberFor<T>,
token_a: H160,
token_b: H160,
standard_amount: u128,
min_output: u128,
},
/// The phantom order bid window was updated
PhantomBidWindowUpdated { window: u32 },
/// The phantom order configuration was updated by governance
PhantomOrderConfigSet { chain: StateMachine, pair_count: u32, interval_blocks: u32 },
}

#[pallet::error]
Expand All @@ -163,6 +206,10 @@ pub mod pallet {
InvalidUserOp,
/// Failed to dispatch cross-chain request
DispatchFailed,
/// A bid was submitted for a phantom order after the acceptance window closed
PhantomOrderBidWindowClosed,
/// A filler already has a bid for this phantom order
DuplicatePhantomBid,
}

#[pallet::call]
Expand Down Expand Up @@ -191,6 +238,22 @@ pub mod pallet {
// Validate user_op is not empty
ensure!(!user_op.is_empty(), Error::<T>::InvalidUserOp);

// Phantom orders have stricter rules: one bid per filler, no updates, and only
// within the configured acceptance window after the order was registered.
if let Some((phantom_commitment, info)) = CurrentPhantomOrder::<T>::get() {
if commitment == phantom_commitment {
let window: BlockNumberFor<T> = Self::phantom_bid_window().into();
ensure!(
frame_system::Pallet::<T>::block_number() <= info.created_at_block + window,
Error::<T>::PhantomOrderBidWindowClosed
);
ensure!(
!Bids::<T>::contains_key(&commitment, &filler),
Error::<T>::DuplicatePhantomBid
);
}
}

// If a bid already exists, unreserve the old deposit first
if let Some(old_deposit) = Bids::<T>::get(&commitment, &filler) {
<T as Config>::Currency::unreserve(&filler, old_deposit);
Expand Down Expand Up @@ -280,10 +343,8 @@ pub mod pallet {
}

// Prepare cross-chain request to notify existing gateway
let new_deployment = types::NewDeployment {
chain: state_machine.to_string().into_bytes(),
gateway,
};
let new_deployment =
types::NewDeployment { chain: state_machine.to_string().into_bytes(), gateway };
let request = RequestKind::AddDeployment(new_deployment);
let body = request.encode_body();

Expand Down Expand Up @@ -443,6 +504,91 @@ pub mod pallet {

Ok(())
}

@Wizdave97 Wizdave97 Jun 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no extrinsic to setup the phantom order details, like token pairs, chain for the order?

/// Set the phantom order configuration. The on_initialize hook reads this every
/// block and generates a new phantom commitment when the interval elapses.
/// Also clears the current active phantom order so the hook fires immediately
/// on the next block.
#[pallet::call_index(7)]
#[pallet::weight(T::WeightInfo::set_phantom_order_config())]
pub fn set_phantom_order_config(
origin: OriginFor<T>,
config: PhantomOrderConfiguration,
) -> DispatchResult {
T::GovernanceOrigin::ensure_origin(origin)?;

let pair_count = config.token_pairs.len() as u32;
let interval_blocks = config.interval_blocks;
let chain = config.chain.clone();

PhantomOrderConfig::<T>::put(&config);
CurrentPhantomOrder::<T>::kill();

Self::deposit_event(Event::PhantomOrderConfigSet {
chain,
pair_count,
interval_blocks,
});

Ok(())
}

/// Update the phantom order bid acceptance window.
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::set_phantom_bid_window())]
pub fn set_phantom_bid_window(origin: OriginFor<T>, window: u32) -> DispatchResult {
T::GovernanceOrigin::ensure_origin(origin)?;

PhantomBidWindow::<T>::put(window);

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

Ok(())
}
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T>
where
T::AccountId: From<[u8; 32]>,
{
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
let Some(config) = PhantomOrderConfig::<T>::get() else {
return Weight::zero();
};

let should_generate = match CurrentPhantomOrder::<T>::get() {
None => true,
Some((_, info)) => {
let interval: BlockNumberFor<T> = config.interval_blocks.into();
!interval.is_zero() && n >= info.created_at_block.saturating_add(interval)
},
};

if !should_generate {
return T::DbWeight::get().reads(2);
}

let chain_bytes = config.chain.to_string().into_bytes();
for pair in config.token_pairs.iter() {
let commitment = Self::compute_phantom_commitment(n, &chain_bytes, pair);
let info = PhantomOrderInfo { created_at_block: n, chain: chain_bytes.clone() };
CurrentPhantomOrder::<T>::put((commitment, info));
Self::deposit_event(Event::PhantomOrderRegistered {
commitment,
chain: chain_bytes.clone(),
created_at: n,
token_a: pair.token_a,
token_b: pair.token_b,
standard_amount: pair.standard_amount,
min_output: pair.min_output,
});
}

T::DbWeight::get()
.reads(2)
.saturating_add(T::DbWeight::get().writes(config.token_pairs.len() as u64 + 1))
}
}

impl<T: Config> Pallet<T>
Expand All @@ -461,11 +607,35 @@ pub mod pallet {
}
}

pub fn phantom_bid_window() -> u32 {
let window = PhantomBidWindow::<T>::get();
if window == 0 {
T::PhantomOrderBidWindowBlocks::get()
} else {
window
}
}

/// Generate offchain storage key for a bid
pub fn offchain_bid_key(commitment: &H256, filler: &T::AccountId) -> Vec<u8> {
offchain_bid_key_raw(commitment, &filler.encode())
}

fn compute_phantom_commitment(
block: BlockNumberFor<T>,
chain: &[u8],
pair: &PhantomTokenPair,
) -> H256 {
types::phantom_order_commitment(
block.saturated_into::<u64>(),
chain,
&pair.token_a,
&pair.token_b,
pair.standard_amount,
pair.min_output,
)
}

/// Dispatch a cross-chain message to a gateway contract
fn dispatch(state_machine: StateMachine, to: H160, body: Vec<u8>) -> DispatchResult {
// Create dispatcher instance
Expand Down
Loading
Loading