Skip to content

Commit 3aafdb5

Browse files
claravanstadenvgeddesacatangiu
authored
Snowbridge - add a linear fee multiplier to ensure safety margins (paritytech#3794)
This is a cherry-pick from master of paritytech#3790 Expected patches for (1.7.0): snowbridge-pallet-ethereum-client snowbridge-pallet-inbound-queue snowbridge-pallet-outbound-queue snowbridge-outbound-queue-runtime-api snowbridge-pallet-system snowbridge-core --------- Co-authored-by: Vincent Geddes <[email protected]> Co-authored-by: claravanstaden <Cats 4 life!> Co-authored-by: Vincent Geddes <[email protected]> Co-authored-by: Adrian Catangiu <[email protected]>
1 parent 486c044 commit 3aafdb5

File tree

12 files changed

+100
-53
lines changed

12 files changed

+100
-53
lines changed

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bridges/snowbridge/parachain/pallets/inbound-queue/src/mock.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ parameter_types! {
182182
pub Parameters: PricingParameters<u128> = PricingParameters {
183183
exchange_rate: FixedU128::from_rational(1, 400),
184184
fee_per_gas: gwei(20),
185-
rewards: Rewards { local: DOT, remote: meth(1) }
185+
rewards: Rewards { local: DOT, remote: meth(1) },
186+
multiplier: FixedU128::from_rational(1, 1),
186187
};
187188
}
188189

bridges/snowbridge/parachain/pallets/outbound-queue/runtime-api/src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
#![cfg_attr(not(feature = "std"), no_std)]
44

55
use frame_support::traits::tokens::Balance as BalanceT;
6-
use snowbridge_core::outbound::Message;
6+
use snowbridge_core::{
7+
outbound::{Command, Fee},
8+
PricingParameters,
9+
};
710
use snowbridge_outbound_queue_merkle_tree::MerkleProof;
811

912
sp_api::decl_runtime_apis! {
1013
pub trait OutboundQueueApi<Balance> where Balance: BalanceT
1114
{
1215
/// Generate a merkle proof for a committed message identified by `leaf_index`.
1316
/// The merkle root is stored in the block header as a
14-
/// `\[`sp_runtime::generic::DigestItem::Other`\]`
17+
/// `sp_runtime::generic::DigestItem::Other`
1518
fn prove_message(leaf_index: u64) -> Option<MerkleProof>;
1619

17-
/// Calculate the delivery fee for `message`
18-
fn calculate_fee(message: Message) -> Option<Balance>;
20+
/// Calculate the delivery fee for `command`
21+
fn calculate_fee(command: Command, parameters: Option<PricingParameters<Balance>>) -> Fee<Balance>;
1922
}
2023
}

bridges/snowbridge/parachain/pallets/outbound-queue/src/api.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
55
use crate::{Config, MessageLeaves};
66
use frame_support::storage::StorageStreamIter;
7-
use snowbridge_core::outbound::{Message, SendMessage};
7+
use snowbridge_core::{
8+
outbound::{Command, Fee, GasMeter},
9+
PricingParameters,
10+
};
811
use snowbridge_outbound_queue_merkle_tree::{merkle_proof, MerkleProof};
12+
use sp_core::Get;
913

1014
pub fn prove_message<T>(leaf_index: u64) -> Option<MerkleProof>
1115
where
@@ -19,12 +23,14 @@ where
1923
Some(proof)
2024
}
2125

22-
pub fn calculate_fee<T>(message: Message) -> Option<T::Balance>
26+
pub fn calculate_fee<T>(
27+
command: Command,
28+
parameters: Option<PricingParameters<T::Balance>>,
29+
) -> Fee<T::Balance>
2330
where
2431
T: Config,
2532
{
26-
match crate::Pallet::<T>::validate(&message) {
27-
Ok((_, fees)) => Some(fees.total()),
28-
_ => None,
29-
}
33+
let gas_used_at_most = T::GasMeter::maximum_gas_used_at_most(&command);
34+
let parameters = parameters.unwrap_or(T::PricingParameters::get());
35+
crate::Pallet::<T>::calculate_fee(gas_used_at_most, parameters)
3036
}

bridges/snowbridge/parachain/pallets/outbound-queue/src/lib.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,37 @@
4747
//! consume on Ethereum. Using this upper bound, a final fee can be calculated.
4848
//!
4949
//! The fee calculation also requires the following parameters:
50-
//! * ETH/DOT exchange rate
51-
//! * Ether fee per unit of gas
50+
//! * Average ETH/DOT exchange rate over some period
51+
//! * Max fee per unit of gas that bridge is willing to refund relayers for
5252
//!
5353
//! By design, it is expected that governance should manually update these
5454
//! parameters every few weeks using the `set_pricing_parameters` extrinsic in the
5555
//! system pallet.
5656
//!
57+
//! This is an interim measure. Once ETH/DOT liquidity pools are available in the Polkadot network,
58+
//! we'll use them as a source of pricing info, subject to certain safeguards.
59+
//!
5760
//! ## Fee Computation Function
5861
//!
5962
//! ```text
6063
//! LocalFee(Message) = WeightToFee(ProcessMessageWeight(Message))
61-
//! RemoteFee(Message) = MaxGasRequired(Message) * FeePerGas + Reward
62-
//! Fee(Message) = LocalFee(Message) + (RemoteFee(Message) / Ratio("ETH/DOT"))
64+
//! RemoteFee(Message) = MaxGasRequired(Message) * Params.MaxFeePerGas + Params.Reward
65+
//! RemoteFeeAdjusted(Message) = Params.Multiplier * (RemoteFee(Message) / Params.Ratio("ETH/DOT"))
66+
//! Fee(Message) = LocalFee(Message) + RemoteFeeAdjusted(Message)
6367
//! ```
6468
//!
65-
//! By design, the computed fee is always going to conservative, to cover worst-case
66-
//! costs of dispatch on Ethereum. In future iterations of the design, we will optimize
67-
//! this, or provide a mechanism to asynchronously refund a portion of collected fees.
69+
//! By design, the computed fee includes a safety factor (the `Multiplier`) to cover
70+
//! unfavourable fluctuations in the ETH/DOT exchange rate.
71+
//!
72+
//! ## Fee Settlement
73+
//!
74+
//! On the remote side, in the gateway contract, the relayer accrues
75+
//!
76+
//! ```text
77+
//! Min(GasPrice, Message.MaxFeePerGas) * GasUsed() + Message.Reward
78+
//! ```
79+
//! Or in plain english, relayers are refunded for gas consumption, using a
80+
//! price that is a minimum of the actual gas price, or `Message.MaxFeePerGas`.
6881
//!
6982
//! # Extrinsics
7083
//!
@@ -106,7 +119,7 @@ pub use snowbridge_outbound_queue_merkle_tree::MerkleProof;
106119
use sp_core::{H256, U256};
107120
use sp_runtime::{
108121
traits::{CheckedDiv, Hash},
109-
DigestItem,
122+
DigestItem, Saturating,
110123
};
111124
use sp_std::prelude::*;
112125
pub use types::{CommittedMessage, ProcessMessageOriginOf};
@@ -366,8 +379,9 @@ pub mod pallet {
366379
// downcast to u128
367380
let fee: u128 = fee.try_into().defensive_unwrap_or(u128::MAX);
368381

369-
// convert to local currency
382+
// multiply by multiplier and convert to local currency
370383
let fee = FixedU128::from_inner(fee)
384+
.saturating_mul(params.multiplier)
371385
.checked_div(&params.exchange_rate)
372386
.expect("exchange rate is not zero; qed")
373387
.into_inner();

bridges/snowbridge/parachain/pallets/outbound-queue/src/mock.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ parameter_types! {
8787
pub Parameters: PricingParameters<u128> = PricingParameters {
8888
exchange_rate: FixedU128::from_rational(1, 400),
8989
fee_per_gas: gwei(20),
90-
rewards: Rewards { local: DOT, remote: meth(1) }
90+
rewards: Rewards { local: DOT, remote: meth(1) },
91+
multiplier: FixedU128::from_rational(4, 3),
9192
};
9293
}
9394

bridges/snowbridge/parachain/pallets/outbound-queue/src/test.rs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -268,42 +268,48 @@ fn encode_digest_item() {
268268
}
269269

270270
#[test]
271-
fn validate_messages_with_fees() {
271+
fn test_calculate_fees_with_unit_multiplier() {
272272
new_tester().execute_with(|| {
273-
let message = mock_message(1000);
274-
let (_, fee) = OutboundQueue::validate(&message).unwrap();
273+
let gas_used: u64 = 250000;
274+
let price_params: PricingParameters<<Test as Config>::Balance> = PricingParameters {
275+
exchange_rate: FixedU128::from_rational(1, 400),
276+
fee_per_gas: 10000_u32.into(),
277+
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
278+
multiplier: FixedU128::from_rational(1, 1),
279+
};
280+
let fee = OutboundQueue::calculate_fee(gas_used, price_params);
275281
assert_eq!(fee.local, 698000000);
276-
assert_eq!(fee.remote, 2680000000000);
282+
assert_eq!(fee.remote, 1000000);
277283
});
278284
}
279285

280286
#[test]
281-
fn test_calculate_fees() {
287+
fn test_calculate_fees_with_multiplier() {
282288
new_tester().execute_with(|| {
283289
let gas_used: u64 = 250000;
284-
let illegal_price_params: PricingParameters<<Test as Config>::Balance> =
285-
PricingParameters {
286-
exchange_rate: FixedU128::from_rational(1, 400),
287-
fee_per_gas: 10000_u32.into(),
288-
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
289-
};
290-
let fee = OutboundQueue::calculate_fee(gas_used, illegal_price_params);
290+
let price_params: PricingParameters<<Test as Config>::Balance> = PricingParameters {
291+
exchange_rate: FixedU128::from_rational(1, 400),
292+
fee_per_gas: 10000_u32.into(),
293+
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
294+
multiplier: FixedU128::from_rational(4, 3),
295+
};
296+
let fee = OutboundQueue::calculate_fee(gas_used, price_params);
291297
assert_eq!(fee.local, 698000000);
292-
assert_eq!(fee.remote, 1000000);
298+
assert_eq!(fee.remote, 1333333);
293299
});
294300
}
295301

296302
#[test]
297303
fn test_calculate_fees_with_valid_exchange_rate_but_remote_fee_calculated_as_zero() {
298304
new_tester().execute_with(|| {
299305
let gas_used: u64 = 250000;
300-
let illegal_price_params: PricingParameters<<Test as Config>::Balance> =
301-
PricingParameters {
302-
exchange_rate: FixedU128::from_rational(1, 1),
303-
fee_per_gas: 1_u32.into(),
304-
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
305-
};
306-
let fee = OutboundQueue::calculate_fee(gas_used, illegal_price_params.clone());
306+
let price_params: PricingParameters<<Test as Config>::Balance> = PricingParameters {
307+
exchange_rate: FixedU128::from_rational(1, 1),
308+
fee_per_gas: 1_u32.into(),
309+
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
310+
multiplier: FixedU128::from_rational(1, 1),
311+
};
312+
let fee = OutboundQueue::calculate_fee(gas_used, price_params.clone());
307313
assert_eq!(fee.local, 698000000);
308314
// Though none zero pricing params the remote fee calculated here is invalid
309315
// which should be avoided

bridges/snowbridge/parachain/pallets/system/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ pub mod pallet {
159159
type DefaultPricingParameters: Get<PricingParametersOf<Self>>;
160160

161161
/// Cost of delivering a message from Ethereum
162+
#[pallet::constant]
162163
type InboundDeliveryCost: Get<BalanceOf<Self>>;
163164

164165
type WeightInfo: WeightInfo;
@@ -334,6 +335,7 @@ pub mod pallet {
334335
let command = Command::SetPricingParameters {
335336
exchange_rate: params.exchange_rate.into(),
336337
delivery_cost: T::InboundDeliveryCost::get().saturated_into::<u128>(),
338+
multiplier: params.multiplier.into(),
337339
};
338340
Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::<T>::No)?;
339341

bridges/snowbridge/parachain/pallets/system/src/mock.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ parameter_types! {
202202
pub Parameters: PricingParameters<u128> = PricingParameters {
203203
exchange_rate: FixedU128::from_rational(1, 400),
204204
fee_per_gas: gwei(20),
205-
rewards: Rewards { local: DOT, remote: meth(1) }
205+
rewards: Rewards { local: DOT, remote: meth(1) },
206+
multiplier: FixedU128::from_rational(4, 3)
206207
};
207208
pub const InboundDeliveryCost: u128 = 1_000_000_000;
208209

bridges/snowbridge/parachain/primitives/core/src/outbound.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ mod v1 {
136136
exchange_rate: UD60x18,
137137
// Cost of delivering a message from Ethereum to BridgeHub, in ROC/KSM/DOT
138138
delivery_cost: u128,
139+
// Fee multiplier
140+
multiplier: UD60x18,
139141
},
140142
}
141143

@@ -203,10 +205,11 @@ mod v1 {
203205
Token::Uint(U256::from(*transfer_asset_xcm)),
204206
Token::Uint(*register_token),
205207
])]),
206-
Command::SetPricingParameters { exchange_rate, delivery_cost } =>
208+
Command::SetPricingParameters { exchange_rate, delivery_cost, multiplier } =>
207209
ethabi::encode(&[Token::Tuple(vec![
208210
Token::Uint(exchange_rate.clone().into_inner()),
209211
Token::Uint(U256::from(*delivery_cost)),
212+
Token::Uint(multiplier.clone().into_inner()),
210213
])]),
211214
}
212215
}
@@ -273,7 +276,8 @@ mod v1 {
273276
}
274277
}
275278

276-
#[cfg_attr(feature = "std", derive(PartialEq, Debug))]
279+
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
280+
#[cfg_attr(feature = "std", derive(PartialEq))]
277281
/// Fee for delivering message
278282
pub struct Fee<Balance>
279283
where
@@ -346,12 +350,13 @@ pub trait GasMeter {
346350
/// the command within the message
347351
const MAXIMUM_BASE_GAS: u64;
348352

353+
/// Total gas consumed at most, including verification & dispatch
349354
fn maximum_gas_used_at_most(command: &Command) -> u64 {
350355
Self::MAXIMUM_BASE_GAS + Self::maximum_dispatch_gas_used_at_most(command)
351356
}
352357

353-
/// Measures the maximum amount of gas a command payload will require to dispatch, AFTER
354-
/// validation & verification.
358+
/// Measures the maximum amount of gas a command payload will require to *dispatch*, NOT
359+
/// including validation & verification.
355360
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64;
356361
}
357362

0 commit comments

Comments
 (0)