-
Notifications
You must be signed in to change notification settings - Fork 97
[intent-coprocessor]: Price Discovery Protocol #684
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 9 commits
b0e0383
fad61b6
c8257eb
004dd9c
cb2c312
9d13e85
6e47e72
6b7dcc1
00d16dd
4fd12d4
fb824aa
789d109
dae9ad7
d925949
9307348
561ad9d
ffc5a1e
f9cbd1d
6762e78
ef25136
a73292f
dcfb1c1
790e716
37d57f3
51bc01d
46c3574
93e5fe6
03dff76
8a3f9cd
b243ee4
80d0398
231c1f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "title": "Intent Gateway", | ||
| "pages": ["overview", "placing-orders", "cancelling-orders", "simplex"], | ||
| "pages": ["overview", "placing-orders", "cancelling-orders", "simplex", "price-submission"], | ||
| "defaultOpen": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| --- | ||
| title: Price Submission Protocol | ||
| description: Deposit-based price submission system for the intents coprocessor pallet | ||
| --- | ||
|
|
||
| # Price Submission Protocol | ||
|
|
||
| ## Overview | ||
|
|
||
| The intents system needs on-chain price data for token pairs to function correctly. The price submission protocol uses a simple deposit-based model: anyone can submit prices for governance-approved token pairs by reserving a deposit on their first submission. Subsequent updates are free, and the deposit can be withdrawn after a configurable lock duration. | ||
|
|
||
| ## How It Works | ||
|
|
||
| The `submit_pair_price` extrinsic on the intents coprocessor pallet accepts price submissions for governance-approved token pairs. All prices and amount ranges are assumed to have 18 decimal places. | ||
|
|
||
| Submissions are batched: each call accepts a `BoundedVec<PriceInput, MaxPriceEntries>` where each entry specifies a base token amount range and the corresponding price. This allows submitters to quote different rates for different order sizes in a single transaction (for example, USDC/CNGN: 0 to 999 at 1414, 1000 to 5000 at 1420). The maximum number of entries per submission is a compile-time constant configurable per runtime via the `MaxPriceEntries` associated type. | ||
|
|
||
| ### Price Entries | ||
|
|
||
| Each `PriceInput` contains three fields: | ||
|
|
||
| - `range_start`: the lower bound of the base token amount range (inclusive), with 18 decimal places. | ||
| - `range_end`: the upper bound of the base token amount range (inclusive), with 18 decimal places. | ||
| - `price`: the price of the base token in terms of the quote token, with 18 decimal places. | ||
|
|
||
| The pallet validates that `range_start <= range_end` for every entry and rejects submissions where this invariant is violated. It also rejects empty submissions. | ||
|
|
||
| When stored on-chain, each entry becomes a `PriceEntry` which adds the submission timestamp. | ||
|
|
||
| ### Deposit Model | ||
|
|
||
| On the first price submission per (account, token pair), a deposit is reserved from the submitter's balance via `ReservableCurrency::reserve()`. The deposit amount is set by governance through the `set_price_deposit_amount` extrinsic. | ||
|
|
||
| After the initial deposit, the submitter can update prices for the same token pair as many times as they want without paying anything further. | ||
|
|
||
| The deposit can be withdrawn after a configurable lock duration has elapsed by calling the `withdraw_price_deposit` extrinsic. The lock duration is set by governance through the `set_price_deposit_lock_duration` extrinsic. Until the lock duration has passed, the deposit remains reserved and cannot be withdrawn. | ||
|
|
||
| This model discourages spam (each new pair requires locking funds) while keeping ongoing price updates free. | ||
|
|
||
| ## Price Window and Data Lifecycle | ||
|
|
||
| Prices are organized into daily windows, which are governance-configurable via `PriceWindowDurationValue`. | ||
|
|
||
| The `on_initialize` hook runs every block and checks whether the current window has expired. When it has, it resets a `PricesClearedThisWindow` flag to false. | ||
|
|
||
| Prices from the previous window are not cleared immediately. They persist so that consumers can still read the previous window's data. On the first new submission in the new window, all price entries across all pairs are cleared before the new entries are stored. This lazy clearing approach avoids the cost of iterating all pairs in `on_initialize` while ensuring stale data is replaced as soon as fresh data arrives. | ||
|
|
||
| ### Why Lazy Clearing | ||
|
|
||
| Clearing all price maps in `on_initialize` would cost weight proportional to the number of pairs with data, which is unbounded and paid by the block producer. Instead, the cost is deferred to the first submitter of the new window, who is already paying for a storage write. The global boolean flag (`PricesClearedThisWindow`) makes this a single check per submission after the first. | ||
|
|
||
| ## Recognized Pairs | ||
|
|
||
| Only governance-approved token pairs can receive price submissions. This prevents spam for arbitrary token combinations and keeps storage growth under control. Governance manages pairs through `add_recognized_pair` and `remove_recognized_pair` extrinsics. Removing a pair also cleans up its associated price data. | ||
|
|
||
| ## RPC | ||
|
|
||
| The `intents_getPairPrices(pair_id)` RPC endpoint returns all prices for a given token pair. Each returned entry includes the amount range (`range_start`, `range_end`), the price, and the submission timestamp. | ||
|
|
||
| ## Governance Parameters | ||
|
|
||
| All key parameters are stored on-chain and updatable via governance extrinsics: | ||
|
|
||
| - `PriceWindowDurationValue` is the length of the price window in milliseconds. | ||
| - `PriceDepositAmount` is the amount reserved from submitters on their first price submission per pair. | ||
| - `PriceDepositLockDuration` is how long (in seconds) the deposit is locked before it can be withdrawn. | ||
| - `MaxPriceEntries` is the compile-time maximum number of price entries per submission, configurable per runtime. | ||
| - Recognized token pairs determine which pairs accept submissions. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,6 +53,19 @@ pub struct RpcBidInfo { | |
| pub user_op: Vec<u8>, | ||
| } | ||
|
|
||
| /// A single price entry returned by the RPC | ||
| #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] | ||
| pub struct RpcPriceEntry { | ||
| /// Lower bound of the base token amount range (inclusive), with 18 decimal places | ||
| pub range_start: String, | ||
| /// Upper bound of the base token amount range (inclusive), with 18 decimal places | ||
| pub range_end: String, | ||
| /// The price of the base token in the quote token, with 18 decimal places | ||
| pub price: String, | ||
| /// Timestamp of submission (seconds) | ||
| pub timestamp: u64, | ||
| } | ||
|
|
||
| impl Ord for RpcBidInfo { | ||
| fn cmp(&self, other: &Self) -> std::cmp::Ordering { | ||
| self.filler.cmp(&other.filler) | ||
|
|
@@ -158,6 +171,17 @@ fn runtime_error_into_rpc_error(e: impl std::fmt::Display) -> ErrorObjectOwned { | |
| ErrorObject::owned(9877, format!("{e}"), None::<String>) | ||
| } | ||
|
|
||
| /// Construct the full storage key for a `StorageMap` entry with `Blake2_128Concat` hasher. | ||
| fn storage_map_key(pallet: &[u8], storage: &[u8], map_key: &H256) -> Vec<u8> { | ||
| let mut key = Vec::new(); | ||
| key.extend_from_slice(&sp_core::hashing::twox_128(pallet)); | ||
| key.extend_from_slice(&sp_core::hashing::twox_128(storage)); | ||
| let map_key_bytes = map_key.as_bytes(); | ||
| key.extend_from_slice(&sp_core::hashing::blake2_128(map_key_bytes)); | ||
| key.extend_from_slice(map_key_bytes); | ||
| key | ||
| } | ||
|
|
||
| /// Construct the storage key prefix for iterating all fillers in the on-chain | ||
| /// `Bids` double-map for a given order commitment. | ||
| fn bids_storage_prefix(commitment: &H256) -> Vec<u8> { | ||
|
|
@@ -176,6 +200,10 @@ pub trait IntentsApi { | |
| #[method(name = "intents_getBidsForOrder")] | ||
| fn get_bids_for_order(&self, commitment: H256) -> RpcResult<Vec<RpcBidInfo>>; | ||
|
|
||
| /// Get all prices for a token pair | ||
| #[method(name = "intents_getPairPrices")] | ||
| fn get_pair_prices(&self, pair_id: H256) -> RpcResult<Vec<RpcPriceEntry>>; | ||
|
|
||
| #[subscription(name = "intents_subscribeBids" => "intents_bidNotification", unsubscribe = "intents_unsubscribeBids", item = RpcBidInfo)] | ||
| async fn subscribe_bids(&self, commitment: Option<H256>) -> SubscriptionResult; | ||
| } | ||
|
|
@@ -259,6 +287,34 @@ where | |
| Ok(bids.into_iter().collect()) | ||
| } | ||
|
|
||
| fn get_pair_prices(&self, pair_id: H256) -> RpcResult<Vec<RpcPriceEntry>> { | ||
| let best_hash = self.client.info().best_hash; | ||
|
|
||
| let key = storage_map_key(b"IntentsCoprocessor", b"Prices", &pair_id); | ||
| let storage_key = sp_core::storage::StorageKey(key); | ||
|
|
||
| let data = match self.client.storage(best_hash, &storage_key) { | ||
| Ok(Some(data)) => data.0, | ||
| _ => return Ok(Vec::new()), | ||
| }; | ||
|
|
||
| // Decode Vec<PriceEntry> | ||
| // PriceEntry SCALE-encodes as (U256, U256, U256, u64) | ||
| type Entry = (primitive_types::U256, primitive_types::U256, primitive_types::U256, u64); | ||
| match Vec::<Entry>::decode(&mut &data[..]) { | ||
| Ok(entries) => Ok(entries | ||
| .into_iter() | ||
| .map(|(range_start, range_end, price, timestamp)| RpcPriceEntry { | ||
| range_start: range_start.to_string(), | ||
|
||
| range_end: range_end.to_string(), | ||
| price: price.to_string(), | ||
| timestamp, | ||
| }) | ||
| .collect()), | ||
| Err(_) => Ok(Vec::new()), | ||
| } | ||
| } | ||
|
|
||
| async fn subscribe_bids( | ||
| &self, | ||
| pending: PendingSubscriptionSink, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.