mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-29 17:17:25 +00:00
22d5b80d44
Bridging fees are calculated using a static ETH/DOT exchange rate that can deviate significantly from the real-world exchange rate. We therefore need to add a safety margin to the fee so that users almost aways cover the cost of relaying. # FAQ > Why introduce a `multiplier` parameter instead of configuring an exchange rate which already has a safety factor applied? When converting from ETH to DOT, we need to _divide_ the multiplier by the exchange rate, and to convert from DOT to ETH we need to _multiply_ the multiplier by the exchange rate. > Other input parameters to the fee calculation can also deviate from real-world values. These include substrate weights, gas prices, and so on. Why does the multiplier introduced here not adjust those? A single scalar multiplier won't be able to accommodate the different volatilities efficiently. For example, gas prices are much more volatile than exchange rates, and substrate weights hardly ever change. So the pricing config relating to weights and gas prices should already have some appropriate safety margin pre-applied. # Detailed Changes: * Added `multiplier` field to `PricingParameters` * Outbound-queue fee is multiplied by `multiplier` * This `multiplier` is synced to the Ethereum side * Improved Runtime API for calculating outbound-queue fees. This API makes it much easier to for configure parts of the system in preparation for launch. * Improve and clarify code documentation Upstreamed from https://github.com/Snowfork/polkadot-sdk/pull/127 --------- Co-authored-by: Clara van Staden <claravanstaden64@gmail.com> Co-authored-by: Adrian Catangiu <adrian@parity.io>
421 lines
14 KiB
Rust
421 lines
14 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
|
//! Pallet for committing outbound messages for delivery to Ethereum
|
|
//!
|
|
//! # Overview
|
|
//!
|
|
//! Messages come either from sibling parachains via XCM, or BridgeHub itself
|
|
//! via the `snowbridge-pallet-system`:
|
|
//!
|
|
//! 1. `snowbridge_router_primitives::outbound::EthereumBlobExporter::deliver`
|
|
//! 2. `snowbridge_pallet_system::Pallet::send`
|
|
//!
|
|
//! The message submission pipeline works like this:
|
|
//! 1. The message is first validated via the implementation for
|
|
//! [`snowbridge_core::outbound::SendMessage::validate`]
|
|
//! 2. The message is then enqueued for later processing via the implementation for
|
|
//! [`snowbridge_core::outbound::SendMessage::deliver`]
|
|
//! 3. The underlying message queue is implemented by [`Config::MessageQueue`]
|
|
//! 4. The message queue delivers messages back to this pallet via the implementation for
|
|
//! [`frame_support::traits::ProcessMessage::process_message`]
|
|
//! 5. The message is processed in `Pallet::do_process_message`: a. Assigned a nonce b. ABI-encoded,
|
|
//! hashed, and stored in the `MessageLeaves` vector
|
|
//! 6. At the end of the block, a merkle root is constructed from all the leaves in `MessageLeaves`.
|
|
//! 7. This merkle root is inserted into the parachain header as a digest item
|
|
//! 8. Offchain relayers are able to relay the message to Ethereum after: a. Generating a merkle
|
|
//! proof for the committed message using the `prove_message` runtime API b. Reading the actual
|
|
//! message content from the `Messages` vector in storage
|
|
//!
|
|
//! On the Ethereum side, the message root is ultimately the thing being
|
|
//! verified by the Polkadot light client.
|
|
//!
|
|
//! # Message Priorities
|
|
//!
|
|
//! The processing of governance commands can never be halted. This effectively
|
|
//! allows us to pause processing of normal user messages while still allowing
|
|
//! governance commands to be sent to Ethereum.
|
|
//!
|
|
//! # Fees
|
|
//!
|
|
//! An upfront fee must be paid for delivering a message. This fee covers several
|
|
//! components:
|
|
//! 1. The weight of processing the message locally
|
|
//! 2. The gas refund paid out to relayers for message submission
|
|
//! 3. An additional reward paid out to relayers for message submission
|
|
//!
|
|
//! Messages are weighed to determine the maximum amount of gas they could
|
|
//! consume on Ethereum. Using this upper bound, a final fee can be calculated.
|
|
//!
|
|
//! The fee calculation also requires the following parameters:
|
|
//! * Average ETH/DOT exchange rate over some period
|
|
//! * Max fee per unit of gas that bridge is willing to refund relayers for
|
|
//!
|
|
//! By design, it is expected that governance should manually update these
|
|
//! parameters every few weeks using the `set_pricing_parameters` extrinsic in the
|
|
//! system pallet.
|
|
//!
|
|
//! This is an interim measure. Once ETH/DOT liquidity pools are available in the Polkadot network,
|
|
//! we'll use them as a source of pricing info, subject to certain safeguards.
|
|
//!
|
|
//! ## Fee Computation Function
|
|
//!
|
|
//! ```text
|
|
//! LocalFee(Message) = WeightToFee(ProcessMessageWeight(Message))
|
|
//! RemoteFee(Message) = MaxGasRequired(Message) * Params.MaxFeePerGas + Params.Reward
|
|
//! RemoteFeeAdjusted(Message) = Params.Multiplier * (RemoteFee(Message) / Params.Ratio("ETH/DOT"))
|
|
//! Fee(Message) = LocalFee(Message) + RemoteFeeAdjusted(Message)
|
|
//! ```
|
|
//!
|
|
//! By design, the computed fee includes a safety factor (the `Multiplier`) to cover
|
|
//! unfavourable fluctuations in the ETH/DOT exchange rate.
|
|
//!
|
|
//! ## Fee Settlement
|
|
//!
|
|
//! On the remote side, in the gateway contract, the relayer accrues
|
|
//!
|
|
//! ```text
|
|
//! Min(GasPrice, Message.MaxFeePerGas) * GasUsed() + Message.Reward
|
|
//! ```
|
|
//! Or in plain english, relayers are refunded for gas consumption, using a
|
|
//! price that is a minimum of the actual gas price, or `Message.MaxFeePerGas`.
|
|
//!
|
|
//! # Extrinsics
|
|
//!
|
|
//! * [`Call::set_operating_mode`]: Set the operating mode
|
|
//!
|
|
//! # Runtime API
|
|
//!
|
|
//! * `prove_message`: Generate a merkle proof for a committed message
|
|
//! * `calculate_fee`: Calculate the delivery fee for a message
|
|
#![cfg_attr(not(feature = "std"), no_std)]
|
|
pub mod api;
|
|
pub mod process_message_impl;
|
|
pub mod send_message_impl;
|
|
pub mod types;
|
|
pub mod weights;
|
|
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
mod benchmarking;
|
|
|
|
#[cfg(test)]
|
|
mod mock;
|
|
|
|
#[cfg(test)]
|
|
mod test;
|
|
|
|
use bridge_hub_common::{AggregateMessageOrigin, CustomDigestItem};
|
|
use codec::Decode;
|
|
use frame_support::{
|
|
storage::StorageStreamIter,
|
|
traits::{tokens::Balance, Contains, Defensive, EnqueueMessage, Get, ProcessMessageError},
|
|
weights::{Weight, WeightToFee},
|
|
};
|
|
use snowbridge_core::{
|
|
outbound::{Fee, GasMeter, QueuedMessage, VersionedQueuedMessage, ETHER_DECIMALS},
|
|
BasicOperatingMode, ChannelId,
|
|
};
|
|
use snowbridge_outbound_queue_merkle_tree::merkle_root;
|
|
pub use snowbridge_outbound_queue_merkle_tree::MerkleProof;
|
|
use sp_core::{H256, U256};
|
|
use sp_runtime::{
|
|
traits::{CheckedDiv, Hash},
|
|
DigestItem, Saturating,
|
|
};
|
|
use sp_std::prelude::*;
|
|
pub use types::{CommittedMessage, ProcessMessageOriginOf};
|
|
pub use weights::WeightInfo;
|
|
|
|
pub use pallet::*;
|
|
|
|
#[frame_support::pallet]
|
|
pub mod pallet {
|
|
use super::*;
|
|
use frame_support::pallet_prelude::*;
|
|
use frame_system::pallet_prelude::*;
|
|
use snowbridge_core::PricingParameters;
|
|
use sp_arithmetic::FixedU128;
|
|
|
|
#[pallet::pallet]
|
|
pub struct Pallet<T>(_);
|
|
|
|
#[pallet::config]
|
|
pub trait Config: frame_system::Config {
|
|
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
|
|
|
type Hashing: Hash<Output = H256>;
|
|
|
|
type MessageQueue: EnqueueMessage<AggregateMessageOrigin>;
|
|
|
|
/// Measures the maximum gas used to execute a command on Ethereum
|
|
type GasMeter: GasMeter;
|
|
|
|
type Balance: Balance + From<u128>;
|
|
|
|
/// Number of decimal places in native currency
|
|
#[pallet::constant]
|
|
type Decimals: Get<u8>;
|
|
|
|
/// Max bytes in a message payload
|
|
#[pallet::constant]
|
|
type MaxMessagePayloadSize: Get<u32>;
|
|
|
|
/// Max number of messages processed per block
|
|
#[pallet::constant]
|
|
type MaxMessagesPerBlock: Get<u32>;
|
|
|
|
/// Check whether a channel exists
|
|
type Channels: Contains<ChannelId>;
|
|
|
|
type PricingParameters: Get<PricingParameters<Self::Balance>>;
|
|
|
|
/// Convert a weight value into a deductible fee based.
|
|
type WeightToFee: WeightToFee<Balance = Self::Balance>;
|
|
|
|
/// Weight information for extrinsics in this pallet
|
|
type WeightInfo: WeightInfo;
|
|
}
|
|
|
|
#[pallet::event]
|
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
|
pub enum Event<T: Config> {
|
|
/// Message has been queued and will be processed in the future
|
|
MessageQueued {
|
|
/// ID of the message. Usually the XCM message hash or a SetTopic.
|
|
id: H256,
|
|
},
|
|
/// Message will be committed at the end of current block. From now on, to track the
|
|
/// progress the message, use the `nonce` of `id`.
|
|
MessageAccepted {
|
|
/// ID of the message
|
|
id: H256,
|
|
/// The nonce assigned to this message
|
|
nonce: u64,
|
|
},
|
|
/// Some messages have been committed
|
|
MessagesCommitted {
|
|
/// Merkle root of the committed messages
|
|
root: H256,
|
|
/// number of committed messages
|
|
count: u64,
|
|
},
|
|
/// Set OperatingMode
|
|
OperatingModeChanged { mode: BasicOperatingMode },
|
|
}
|
|
|
|
#[pallet::error]
|
|
pub enum Error<T> {
|
|
/// The message is too large
|
|
MessageTooLarge,
|
|
/// The pallet is halted
|
|
Halted,
|
|
/// Invalid Channel
|
|
InvalidChannel,
|
|
}
|
|
|
|
/// Messages to be committed in the current block. This storage value is killed in
|
|
/// `on_initialize`, so should never go into block PoV.
|
|
///
|
|
/// Is never read in the runtime, only by offchain message relayers.
|
|
///
|
|
/// Inspired by the `frame_system::Pallet::Events` storage value
|
|
#[pallet::storage]
|
|
#[pallet::unbounded]
|
|
pub(super) type Messages<T: Config> = StorageValue<_, Vec<CommittedMessage>, ValueQuery>;
|
|
|
|
/// Hashes of the ABI-encoded messages in the [`Messages`] storage value. Used to generate a
|
|
/// merkle root during `on_finalize`. This storage value is killed in
|
|
/// `on_initialize`, so should never go into block PoV.
|
|
#[pallet::storage]
|
|
#[pallet::unbounded]
|
|
#[pallet::getter(fn message_leaves)]
|
|
pub(super) type MessageLeaves<T: Config> = StorageValue<_, Vec<H256>, ValueQuery>;
|
|
|
|
/// The current nonce for each message origin
|
|
#[pallet::storage]
|
|
pub type Nonce<T: Config> = StorageMap<_, Twox64Concat, ChannelId, u64, ValueQuery>;
|
|
|
|
/// The current operating mode of the pallet.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn operating_mode)]
|
|
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
|
|
|
|
#[pallet::hooks]
|
|
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T>
|
|
where
|
|
T::AccountId: AsRef<[u8]>,
|
|
{
|
|
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
|
|
// Remove storage from previous block
|
|
Messages::<T>::kill();
|
|
MessageLeaves::<T>::kill();
|
|
// Reserve some weight for the `on_finalize` handler
|
|
T::WeightInfo::commit()
|
|
}
|
|
|
|
fn on_finalize(_: BlockNumberFor<T>) {
|
|
Self::commit();
|
|
}
|
|
|
|
fn integrity_test() {
|
|
let decimals = T::Decimals::get();
|
|
assert!(decimals == 10 || decimals == 12, "Decimals should be 10 or 12");
|
|
}
|
|
}
|
|
|
|
#[pallet::call]
|
|
impl<T: Config> Pallet<T> {
|
|
/// Halt or resume all pallet operations. May only be called by root.
|
|
#[pallet::call_index(0)]
|
|
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
|
|
pub fn set_operating_mode(
|
|
origin: OriginFor<T>,
|
|
mode: BasicOperatingMode,
|
|
) -> DispatchResult {
|
|
ensure_root(origin)?;
|
|
OperatingMode::<T>::put(mode);
|
|
Self::deposit_event(Event::OperatingModeChanged { mode });
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<T: Config> Pallet<T> {
|
|
/// Generate a messages commitment and insert it into the header digest
|
|
pub(crate) fn commit() {
|
|
let count = MessageLeaves::<T>::decode_len().unwrap_or_default() as u64;
|
|
if count == 0 {
|
|
return
|
|
}
|
|
|
|
// Create merkle root of messages
|
|
let root = merkle_root::<<T as Config>::Hashing, _>(MessageLeaves::<T>::stream_iter());
|
|
|
|
let digest_item: DigestItem = CustomDigestItem::Snowbridge(root).into();
|
|
|
|
// Insert merkle root into the header digest
|
|
<frame_system::Pallet<T>>::deposit_log(digest_item);
|
|
|
|
Self::deposit_event(Event::MessagesCommitted { root, count });
|
|
}
|
|
|
|
/// Process a message delivered by the MessageQueue pallet
|
|
pub(crate) fn do_process_message(
|
|
_: ProcessMessageOriginOf<T>,
|
|
mut message: &[u8],
|
|
) -> Result<bool, ProcessMessageError> {
|
|
use ProcessMessageError::*;
|
|
|
|
// Yield if the maximum number of messages has been processed this block.
|
|
// This ensures that the weight of `on_finalize` has a known maximum bound.
|
|
ensure!(
|
|
MessageLeaves::<T>::decode_len().unwrap_or(0) <
|
|
T::MaxMessagesPerBlock::get() as usize,
|
|
Yield
|
|
);
|
|
|
|
// Decode bytes into versioned message
|
|
let versioned_queued_message: VersionedQueuedMessage =
|
|
VersionedQueuedMessage::decode(&mut message).map_err(|_| Corrupt)?;
|
|
|
|
// Convert versioned message into latest supported message version
|
|
let queued_message: QueuedMessage =
|
|
versioned_queued_message.try_into().map_err(|_| Unsupported)?;
|
|
|
|
// Obtain next nonce
|
|
let nonce = <Nonce<T>>::try_mutate(
|
|
queued_message.channel_id,
|
|
|nonce| -> Result<u64, ProcessMessageError> {
|
|
*nonce = nonce.checked_add(1).ok_or(Unsupported)?;
|
|
Ok(*nonce)
|
|
},
|
|
)?;
|
|
|
|
let pricing_params = T::PricingParameters::get();
|
|
let command = queued_message.command.index();
|
|
let params = queued_message.command.abi_encode();
|
|
let max_dispatch_gas =
|
|
T::GasMeter::maximum_dispatch_gas_used_at_most(&queued_message.command);
|
|
let reward = pricing_params.rewards.remote;
|
|
|
|
// Construct the final committed message
|
|
let message = CommittedMessage {
|
|
channel_id: queued_message.channel_id,
|
|
nonce,
|
|
command,
|
|
params,
|
|
max_dispatch_gas,
|
|
max_fee_per_gas: pricing_params
|
|
.fee_per_gas
|
|
.try_into()
|
|
.defensive_unwrap_or(u128::MAX),
|
|
reward: reward.try_into().defensive_unwrap_or(u128::MAX),
|
|
id: queued_message.id,
|
|
};
|
|
|
|
// ABI-encode and hash the prepared message
|
|
let message_abi_encoded = ethabi::encode(&[message.clone().into()]);
|
|
let message_abi_encoded_hash = <T as Config>::Hashing::hash(&message_abi_encoded);
|
|
|
|
Messages::<T>::append(Box::new(message));
|
|
MessageLeaves::<T>::append(message_abi_encoded_hash);
|
|
|
|
Self::deposit_event(Event::MessageAccepted { id: queued_message.id, nonce });
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Calculate total fee in native currency to cover all costs of delivering a message to the
|
|
/// remote destination. See module-level documentation for more details.
|
|
pub(crate) fn calculate_fee(
|
|
gas_used_at_most: u64,
|
|
params: PricingParameters<T::Balance>,
|
|
) -> Fee<T::Balance> {
|
|
// Remote fee in ether
|
|
let fee = Self::calculate_remote_fee(
|
|
gas_used_at_most,
|
|
params.fee_per_gas,
|
|
params.rewards.remote,
|
|
);
|
|
|
|
// downcast to u128
|
|
let fee: u128 = fee.try_into().defensive_unwrap_or(u128::MAX);
|
|
|
|
// multiply by multiplier and convert to local currency
|
|
let fee = FixedU128::from_inner(fee)
|
|
.saturating_mul(params.multiplier)
|
|
.checked_div(¶ms.exchange_rate)
|
|
.expect("exchange rate is not zero; qed")
|
|
.into_inner();
|
|
|
|
// adjust fixed point to match local currency
|
|
let fee = Self::convert_from_ether_decimals(fee);
|
|
|
|
Fee::from((Self::calculate_local_fee(), fee))
|
|
}
|
|
|
|
/// Calculate fee in remote currency for dispatching a message on Ethereum
|
|
pub(crate) fn calculate_remote_fee(
|
|
gas_used_at_most: u64,
|
|
fee_per_gas: U256,
|
|
reward: U256,
|
|
) -> U256 {
|
|
fee_per_gas.saturating_mul(gas_used_at_most.into()).saturating_add(reward)
|
|
}
|
|
|
|
/// The local component of the message processing fees in native currency
|
|
pub(crate) fn calculate_local_fee() -> T::Balance {
|
|
T::WeightToFee::weight_to_fee(
|
|
&T::WeightInfo::do_process_message().saturating_add(T::WeightInfo::commit_single()),
|
|
)
|
|
}
|
|
|
|
// 1 DOT has 10 digits of precision
|
|
// 1 KSM has 12 digits of precision
|
|
// 1 ETH has 18 digits of precision
|
|
pub(crate) fn convert_from_ether_decimals(value: u128) -> T::Balance {
|
|
let decimals = ETHER_DECIMALS.saturating_sub(T::Decimals::get()) as u32;
|
|
let denom = 10u128.saturating_pow(decimals);
|
|
value.checked_div(denom).expect("divisor is non-zero; qed").into()
|
|
}
|
|
}
|
|
}
|