Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b0e0383
allow fillers to sumbmit pair price using membership and non membersh…
dharjeezy Mar 11, 2026
fad61b6
vec
dharjeezy Mar 11, 2026
c8257eb
Merge branch 'main' of github.com:polytope-labs/hyperbridge into dami…
dharjeezy Mar 12, 2026
004dd9c
dual-path price submission system for verified prices and unverified …
dharjeezy Mar 12, 2026
cb2c312
TreasuryAccount in config
dharjeezy Mar 12, 2026
9d13e85
fmt
dharjeezy Mar 12, 2026
6e47e72
introduce price range input, include docs
dharjeezy Mar 13, 2026
6b7dcc1
cross-chain filler verification by inspecting RedeemEscrow messages …
dharjeezy Mar 13, 2026
00d16dd
fully reserve deposit price submission
dharjeezy Mar 16, 2026
4fd12d4
simplex integration for price update
dharjeezy Mar 16, 2026
fb824aa
address concerns
dharjeezy Mar 16, 2026
789d109
tested out implementation
dharjeezy Mar 17, 2026
dae9ad7
update to doc
dharjeezy Mar 17, 2026
d925949
block price submissions when a withdrawal is in progress, use H256 fo…
dharjeezy Mar 17, 2026
9307348
Merge main: resolve conflicts in FX strategy and simplex config
dharjeezy Mar 17, 2026
561ad9d
remove service
dharjeezy Mar 17, 2026
ffc5a1e
use price policy to get points
dharjeezy Mar 17, 2026
f9cbd1d
clear stale prices per pair, compute pair ids automatically, submit a…
dharjeezy Mar 18, 2026
6762e78
simplify pair IDs to keccak256("base/quote"), replace timestamp with …
dharjeezy Mar 18, 2026
ef25136
benchmarks, simplified price entries
dharjeezy Mar 18, 2026
a73292f
configurable stablecoin addresses, update simtest, and add per-entry …
dharjeezy Mar 19, 2026
dcfb1c1
update docs
dharjeezy Mar 19, 2026
790e716
getQuotes method
dharjeezy Mar 20, 2026
37d57f3
Merge branches 'dami/filler-price-pair-update' and 'main' of github.c…
dharjeezy Mar 20, 2026
51bc01d
refacoring
dharjeezy Mar 20, 2026
46c3574
introduce side to differentiate prices for asks and bids,
dharjeezy Mar 20, 2026
93e5fe6
interpolate
dharjeezy Mar 20, 2026
03dff76
revert side
dharjeezy Mar 20, 2026
8a3f9cd
register pair via reserve deposits
dharjeezy Mar 23, 2026
b243ee4
Merge branch 'main' of github.com:polytope-labs/hyperbridge into dami…
dharjeezy Mar 24, 2026
80d0398
fee based submission
dharjeezy Mar 24, 2026
231c1f2
filter 24 hours prices
dharjeezy Mar 24, 2026
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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/content/developers/intent-gateway/meta.json
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
}
68 changes: 68 additions & 0 deletions docs/content/developers/intent-gateway/price-submission.mdx
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.
3 changes: 3 additions & 0 deletions modules/pallets/intents-coprocessor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ anyhow = { workspace = true }
alloy-primitives = { workspace = true }
alloy-sol-macro = { workspace = true }
alloy-sol-types = { workspace = true }
hex-literal = { workspace = true }

crypto-utils = { workspace = true }
ismp = { workspace = true }
pallet-ismp = { workspace = true }

Expand All @@ -49,6 +51,7 @@ std = [
"scale-info/std",
"anyhow/std",
"alloy-primitives/std",
"crypto-utils/std",
"sp-io/std",
"pallet-ismp/std",
]
Expand Down
1 change: 1 addition & 0 deletions modules/pallets/intents-coprocessor/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ log = { workspace = true, default-features = true }
tokio = { workspace = true, features = ["sync", "time"] }
futures = { workspace = true }
hex = { workspace = true, default-features = true }
primitive-types = { workspace = true, default-features = true }
pallet-intents-coprocessor = { workspace = true, default-features = true }

[dependencies.polkadot-sdk]
Expand Down
56 changes: 56 additions & 0 deletions modules/pallets/intents-coprocessor/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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> {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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(),
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.

Let's divide the prices by 10**18 here to cnvert it to human readable value before returning

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.

Preserve the decimals during this conversion

range_end: range_end.to_string(),
price: price.to_string(),
timestamp,
})
.collect()),
Err(_) => Ok(Vec::new()),
}
}

async fn subscribe_bids(
&self,
pending: PendingSubscriptionSink,
Expand Down
Loading
Loading