Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
}
70 changes: 70 additions & 0 deletions docs/content/developers/intent-gateway/price-submission.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Price Submission Protocol
description: Dual-path 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 challenge is sourcing reliable prices without depending on external oracles, while still ensuring data availability when verified sources are temporarily scarce.

## Protocol Overview

The `submit_pair_price` extrinsic on the intents coprocessor pallet accepts price submissions for governance-approved token pairs. It supports two confidence levels through a single entry point, distinguished by an optional `verification` parameter. All prices are assumed to have 18 decimal places.

### Verified Prices (High Confidence)

Fillers who have actually filled orders on the IntentGateway contract submit prices along with cryptographic proof of their fill. This path exists because fillers are the most trustworthy source of price data. They have real skin in the game and their prices reflect actual market activity.

A verified submission requires three things:

1. **Two ISMP state proofs** against the IntentGateway contract's `_filled` mapping. A non-membership proof at height H1 shows the order was not yet filled, and a membership proof at height H2 shows it was filled. Together these bracket when the fill occurred and extract the filler's EVM address from storage.

2. **Proof freshness.** Both the gap between H1 and H2, and the age of H2 relative to the current time, must fall within a governance-configured threshold. This prevents stale or manufactured proofs from being accepted.

3. **An EVM signature** over `keccak256(SCALE_encode(nonce, pair_id, price))`. This proves the substrate account submitting the price actually controls the EVM account that performed the fill. The nonce is tracked on-chain per EVM address to prevent signature replay. This pattern follows the existing approach in `pallet-ismp-relayer`.

The 52-byte proof key binds verification to a specific gateway contract. The first 20 bytes are the gateway address (used by the EVM state machine client to locate the contract in the world state trie) and the last 32 bytes are the hashed storage slot for `_filled[commitment]`.

Verified prices are stored in `VerifiedPrices` with no cap. The goal is to accumulate as many real data points as possible within each price window.

### Unverified Prices (Low Confidence)

Anyone can submit a price without proofs by paying a fee in bridge tokens. This path exists to maintain price data availability even when verified submissions are sparse. The fee, which is transferred to the treasury, discourages spam while keeping the door open for market participants who have price information but haven't filled orders themselves.

Unverified prices are stored separately in `UnverifiedPrices` and capped per pair at `MaxUnverifiedSubmissions`. When the cap is reached, the oldest entry is replaced using a first-in, first-out policy. Both the fee and the cap must be configured via governance for unverified submissions to be accepted.

### Why Two Confidence Levels

Keeping verified and unverified prices separate lets consumers make informed decisions. A DEX might only trust verified prices for settlement, while a UI might display both to give users a fuller picture. Mixing them into a single pool would dilute the signal from proven fills.

## 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 clears `UsedCommitments` (which is safe because the freshness threshold independently rejects stale proofs) and resets a `PricesClearedThisWindow` flag to false.

Prices from the previous window are not cleared immediately. They persist so that consumers can still read yesterday's data. On the first new submission in the new window (verified or unverified), all price entries across all pairs are cleared before the new entry is 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 verified and unverified prices separately, allowing consumers to apply their own weighting or filtering based on confidence level.

## Governance Parameters

All key parameters are stored on-chain and updatable via governance extrinsics:

- `PriceWindowDurationValue` is the length of the price window in milliseconds.
- `ProofFreshnessThresholdValue` is the maximum allowed age and gap for state proofs in seconds.
- `MaxUnverifiedSubmissions` is the cap on unverified entries per pair.
- `UnverifiedSubmissionFee` is the fee charged for unverified submissions.
- 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
82 changes: 82 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,31 @@ 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 {
/// The filler's EVM address (zero address for unverified submissions)
#[serde(with = "hex_bytes")]
pub filler: Vec<u8>,
/// 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,
}

/// Response for the `intents_getPairPrices` RPC method
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct RpcPairPrices {
/// High confidence prices (from verified fillers with proofs)
pub verified: Vec<RpcPriceEntry>,
/// Low confidence prices (from unverified submitters)
pub unverified: Vec<RpcPriceEntry>,
}

impl Ord for RpcBidInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.filler.cmp(&other.filler)
Expand Down Expand Up @@ -158,6 +183,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 +212,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, separated by confidence level
#[method(name = "intents_getPairPrices")]
fn get_pair_prices(&self, pair_id: H256) -> RpcResult<RpcPairPrices>;

#[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 +299,48 @@ where
Ok(bids.into_iter().collect())
}

fn get_pair_prices(&self, pair_id: H256) -> RpcResult<RpcPairPrices> {
let best_hash = self.client.info().best_hash;

let decode_entries = |storage_name: &[u8]| -> Vec<RpcPriceEntry> {
let key = storage_map_key(b"IntentsCoprocessor", storage_name, &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 Vec::new(),
};

// Decode Vec<PriceEntry>
// PriceEntry SCALE-encodes as (H160, U256, U256, U256, u64)
type Entry = (
primitive_types::H160,
primitive_types::U256,
primitive_types::U256,
primitive_types::U256,
u64,
);
match Vec::<Entry>::decode(&mut &data[..]) {
Ok(entries) => entries
.into_iter()
.map(|(filler, range_start, range_end, price, timestamp)| RpcPriceEntry {
filler: filler.as_bytes().to_vec(),
range_start: range_start.to_string(),
range_end: range_end.to_string(),
price: price.to_string(),
timestamp,
})
.collect(),
Err(_) => Vec::new(),
}
};

Ok(RpcPairPrices {
verified: decode_entries(b"VerifiedPrices"),
unverified: decode_entries(b"UnverifiedPrices"),
})
}

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