Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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);
}
165 changes: 160 additions & 5 deletions modules/pallets/intents-coprocessor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ use sp_io::offchain_index;
use sp_runtime::traits::{ConstU32, Zero};
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 +67,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 +87,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 +128,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 +173,12 @@ 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> },
/// 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 +195,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 +227,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 +332,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 +493,87 @@ 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,
});
}

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 +592,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 {
let mut preimage = Vec::new();

@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.

The phantom order should be an actual intent gateway order.

preimage.extend_from_slice(chain);
preimage.extend_from_slice(pair.token_a.as_bytes());
preimage.extend_from_slice(pair.token_b.as_bytes());
preimage.extend_from_slice(&pair.standard_amount.to_be_bytes());
preimage.extend_from_slice(&pair.min_output.to_be_bytes());
preimage.extend_from_slice(&block.encode());
sp_io::hashing::keccak_256(&preimage).into()
}

/// 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
29 changes: 28 additions & 1 deletion modules/pallets/intents-coprocessor/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
use alloc::{vec, vec::Vec};
use alloy_sol_types::SolValue;
use codec::{Decode, DecodeWithMemTracking, Encode};

use ismp::host::StateMachine;
use polkadot_sdk::frame_support::{traits::ConstU32, BoundedVec};
use primitive_types::{H160, H256, U256};
use scale_info::TypeInfo;

Expand Down Expand Up @@ -135,6 +136,32 @@ pub struct GatewayInfo {
pub params: IntentGatewayParams,
}

/// Tracks the single active phantom order recognised by the pallet.
#[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Eq)]
pub struct PhantomOrderInfo<BlockNumber> {
pub created_at_block: BlockNumber,
/// Raw state machine identifier bytes (e.g. b"EVM-8453").
pub chain: Vec<u8>,
}

/// A single token pair the phantom generator probes for price and liquidity.
#[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Eq)]
pub struct PhantomTokenPair {
pub token_a: H160,
pub token_b: H160,
pub standard_amount: u128,
pub min_output: u128,
}

/// Governance-settable configuration for autonomous phantom order generation.
/// Stored in `PhantomOrderConfig`; the pallet hook reads it every block.
#[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Eq)]
pub struct PhantomOrderConfiguration {
pub chain: StateMachine,
pub token_pairs: BoundedVec<PhantomTokenPair, ConstU32<10>>,
pub interval_blocks: u32,
}

/// A bid placed by a filler for an order
#[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Eq)]
pub struct Bid<AccountId> {
Expand Down
24 changes: 24 additions & 0 deletions modules/pallets/intents-coprocessor/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub trait WeightInfo {
fn update_params() -> Weight;
fn sweep_dust() -> Weight;
fn update_token_decimals() -> Weight;
fn set_phantom_order_config() -> Weight;
fn set_phantom_bid_window() -> Weight;
}

/// Weights for pallet_intents using the Substrate node and recommended hardware.
Expand Down Expand Up @@ -105,6 +107,22 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
.saturating_add(T::DbWeight::get().reads(2))
.saturating_add(T::DbWeight::get().writes(1))
}

/// Storage: PhantomOrderConfig (r:0 w:1), CurrentPhantomOrder (r:0 w:1)
/// Proof Skipped: PhantomOrderConfig (max_values: Some(1), max_size: None, mode: Measured)
fn set_phantom_order_config() -> Weight {
Weight::from_parts(20_000_000, 0)
.saturating_add(Weight::from_parts(0, 1_024))
.saturating_add(T::DbWeight::get().writes(2))
}

/// Storage: PhantomBidWindow (r:0 w:1)
/// Proof Skipped: PhantomBidWindow (max_values: Some(1), max_size: Some(4), mode: Measured)
fn set_phantom_bid_window() -> Weight {
// Measured on reference hardware; re-run benchmarks after schema changes.
Weight::from_parts(10_000_000, 0)
.saturating_add(T::DbWeight::get().writes(1))
}
}

// For backwards compatibility and tests
Expand All @@ -127,4 +145,10 @@ impl WeightInfo for () {
fn update_token_decimals() -> Weight {
Weight::from_parts(75_000_000, 0)
}
fn set_phantom_order_config() -> Weight {
Weight::from_parts(20_000_000, 0)
}
fn set_phantom_bid_window() -> Weight {
Weight::from_parts(10_000_000, 0)
}
}
2 changes: 2 additions & 0 deletions parachain/runtimes/gargantua/src/ismp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,14 @@ impl pallet_state_coprocessor::Config for Runtime {

parameter_types! {
pub const IntentStorageDepositFee: Balance = 100 * EXISTENTIAL_DEPOSIT;
pub const IntentPhantomOrderBidWindow: u32 = 15;
}

impl pallet_intents_coprocessor::Config for Runtime {
type Dispatcher = Ismp;
type Currency = Balances;
type StorageDepositFee = IntentStorageDepositFee;
type PhantomOrderBidWindowBlocks = IntentPhantomOrderBidWindow;
type GovernanceOrigin = EnsureRoot<AccountId>;
type WeightInfo = weights::pallet_intents_coprocessor::WeightInfo<Runtime>;
}
Expand Down
Loading
Loading