-
Notifications
You must be signed in to change notification settings - Fork 122
[runtime/simplex]]: add phantom order support for passive price and liquidity indexing #983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
62c3163
e26564e
4460afb
6eb80a1
f00dad3
eb273ac
3d34405
d23b75d
ce46fcb
b1a15c9
dac3c5d
3795ea7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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::*; | ||
|
|
@@ -58,12 +64,21 @@ pub fn offchain_bid_key_raw(commitment: &H256, filler_encoded: &[u8]) -> Vec<u8> | |
| key | ||
| } | ||
|
|
||
| /// Generate the offchain storage key for the ABI-encoded phantom order, keyed by commitment. | ||
| pub fn offchain_phantom_key(commitment: &H256) -> Vec<u8> { | ||
| let mut key = b"intents::phantom::order::".to_vec(); | ||
| key.extend_from_slice(commitment.as_bytes()); | ||
| key | ||
| } | ||
|
|
||
| #[frame_support::pallet] | ||
| pub mod pallet { | ||
| use super::*; | ||
| use crate::alloc::string::ToString; | ||
| use frame_support::pallet_prelude::*; | ||
| use frame_support::traits::UnixTime; | ||
| use frame_system::pallet_prelude::*; | ||
| use polkadot_sdk::sp_runtime::traits::Saturating; | ||
|
|
||
| #[pallet::pallet] | ||
| #[pallet::without_storage_info] | ||
|
|
@@ -83,6 +98,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>; | ||
|
|
||
|
|
@@ -119,6 +139,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> { | ||
|
|
@@ -147,6 +184,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] | ||
|
|
@@ -163,6 +214,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] | ||
|
|
@@ -191,6 +246,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); | ||
|
|
@@ -280,10 +351,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(); | ||
|
|
||
|
|
@@ -443,6 +512,97 @@ pub mod pallet { | |
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// 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); | ||
| } | ||
|
|
||
| // Use pallet-ismp's time provider so the deadline is a real chain timestamp | ||
| // (non-zero). Simulation uses block 0 (timestamp=0) so the check still passes. | ||
| let deadline_secs = <T as pallet_ismp::Config>::TimestampProvider::now().as_secs(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deadline is in blocks, I meant fetch the latest state machine height for the order chain and use that as the deadline |
||
|
|
||
| let chain_bytes = config.chain.to_string().into_bytes(); | ||
| for pair in config.token_pairs.iter() { | ||
| let (commitment, order_bytes) = | ||
| Self::compute_phantom_commitment(n, &chain_bytes, pair, deadline_secs); | ||
| let info = PhantomOrderInfo { created_at_block: n, chain: chain_bytes.clone() }; | ||
| CurrentPhantomOrder::<T>::put((commitment, info)); | ||
| offchain_index::set(&offchain_phantom_key(&commitment), &order_bytes); | ||
| 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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this min_output config, it's not useful |
||
| }); | ||
| } | ||
|
|
||
| T::DbWeight::get() | ||
| .reads(2) | ||
| .saturating_add(T::DbWeight::get().writes(config.token_pairs.len() as u64 + 1)) | ||
| } | ||
| } | ||
|
|
||
| impl<T: Config> Pallet<T> | ||
|
|
@@ -461,11 +621,36 @@ 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, | ||
| deadline_secs: u64, | ||
| ) -> (H256, Vec<u8>) { | ||
| types::phantom_order_commitment( | ||
| block.saturated_into::<u64>(), | ||
| chain, | ||
| &pair.token_a, | ||
| &pair.token_b, | ||
| pair.standard_amount, | ||
| deadline_secs, | ||
| ) | ||
| } | ||
|
|
||
| /// Dispatch a cross-chain message to a gateway contract | ||
| fn dispatch(state_machine: StateMachine, to: H160, body: Vec<u8>) -> DispatchResult { | ||
| // Create dispatcher instance | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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?