Skip to content
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

Refactoring XcmExportFeeToSibling and more tests for outbound fee #1109

Merged
merged 27 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3ac5f12
Handle fee with error instead of silently failing
yrong Jan 10, 2024
319d4ba
Merge branch 'main' into ron/handle-fee-with-error
yrong Jan 10, 2024
279dbf2
Fix for pallet rename
yrong Jan 10, 2024
7f7e4bc
AssetTransactor::deposit_asset directly with error thrown
yrong Jan 11, 2024
4ddca9c
Move invalid params to test
yrong Jan 11, 2024
965e579
Check more strictly
yrong Jan 11, 2024
5fb402b
More tests
yrong Jan 11, 2024
3894746
More tests
yrong Jan 11, 2024
8361b26
Handle fee without error
yrong Jan 11, 2024
79f79f1
Update sdk
yrong Jan 11, 2024
3fa6fcd
More tests for outbound fee
yrong Jan 11, 2024
76f4d4f
More tests
yrong Jan 11, 2024
d359e19
Update parachain/runtime/runtime-common/src/lib.rs
yrong Jan 12, 2024
6ca0929
Update parachain/runtime/runtime-common/src/lib.rs
yrong Jan 12, 2024
5922a5e
Update parachain/runtime/runtime-common/src/lib.rs
yrong Jan 12, 2024
122a5a2
Update parachain/runtime/runtime-common/src/lib.rs
yrong Jan 12, 2024
986f19e
Merge branch 'main' into ron/handle-fee
yrong Jan 12, 2024
c789e69
Merge branch 'ron/handle-fee' of https://github.com/Snowfork/snowbrid…
yrong Jan 12, 2024
7514bd1
More param checks & more tests
yrong Jan 16, 2024
18daf99
Rename test case less confusing
yrong Jan 16, 2024
f43fce3
Remove sanity check for Pricing params
yrong Jan 16, 2024
711720c
Remove test unnecessary
yrong Jan 16, 2024
de03393
Revert to unroutable
yrong Jan 17, 2024
85cb976
Merge branch 'main' into ron/handle-fee
yrong Jan 17, 2024
af15053
Revert the checks
yrong Jan 17, 2024
122572d
Rename as MockAssetTransactor
yrong Jan 17, 2024
15132aa
Update sdk
yrong Jan 19, 2024
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
2 changes: 2 additions & 0 deletions parachain/Cargo.lock

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

8 changes: 8 additions & 0 deletions parachain/pallets/outbound-queue/src/send_message_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use snowbridge_core::{
},
ChannelId, PRIMARY_GOVERNANCE_CHANNEL,
};
use sp_arithmetic::traits::Zero;
use sp_core::H256;
use sp_runtime::BoundedVec;

Expand Down Expand Up @@ -57,7 +58,14 @@ where
.unwrap_or_else(|| unique((message.channel_id, &message.command)).into());

let gas_used_at_most = T::GasMeter::maximum_gas_used_at_most(&message.command);
ensure!(
gas_used_at_most > T::GasMeter::MAXIMUM_BASE_GAS &&
gas_used_at_most < T::GasMeter::MAXIMUM_GAS_CAP,
SendError::MessageInvalidGasFees
);
let fee = Self::calculate_fee(gas_used_at_most, T::PricingParameters::get());
let zero_fee = <Self as SendMessageFeeProvider>::Balance::zero();
ensure!(fee.local > zero_fee && fee.remote > zero_fee, SendError::MessageInvalidGasFees);
Copy link
Collaborator

@vgeddes vgeddes Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These checks don't make sense to me. The expectation is that T::GasMeter::maximum_gas_used_at_most and Self::calculate_fee always return correct values for any arbitrary Command.

This behavior is important, as we can't reject commands because for some reason we could not calculate fees. To do otherwise would lead to a loss of user funds.

It seems these checks are acting as unit tests, and so we should convert them to actual unit tests.

Copy link
Contributor Author

@yrong yrong Jan 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check fee.remote > 0 is for the test case as I mentioned here in #1109 (comment) in which the remote_fee calculated being zero(though pricing parameters are all non-zero and valid). It will lead to the sovereign account of the sibling chain not being properly refunded in XcmExportFeeToSibling and finally the agent on the Ethereum side can't be funded.

That's also the reason why I introduced the sanity check of the pricing params, prefer to reject commands because for some reason we could not calculate fees rather than silently ignoring.

Copy link
Contributor Author

@yrong yrong Jan 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAXIMUM_GAS_CAP added here is a hard limit that applies to all commands. It's mainly for command Upgrade with a sanity initializer_max_gas. It also applies to the future arbitrary transact for which the gas is dynamic from the input of the end-user.

MAXIMUM_GAS_CAP should be consistent with the default relayer config otherwise the check in Gateway will always fail and the bridge stuck.

Copy link
Collaborator

@vgeddes vgeddes Jan 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check fee.remote > 0 is for the test case as I mentioned here in #1109 (comment) in which the remote_fee calculated being zero(though pricing parameters are all non-zero and valid). It will lead to the sovereign account of the sibling chain not being properly refunded in XcmExportFeeToSibling and finally the agent on the Ethereum side can't be funded.

Thanks Ron, that makes sense. But it seems there is a bug in our code if the calculated remote fee ends up being zero. Can you investigate why this happens? We should try and fix it so that we don't need to have these checks.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should introduce MAXIMUM_GAS_CAP, for the same reasons I outlined in #1109 (comment).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #1109 (comment). I think its fine if calculated remote fee is zero. It just means the fee is too low to be represented using DOT decimals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checks reverted in af15053


let queued_message: VersionedQueuedMessage = QueuedMessage {
id: message_id,
Expand Down
49 changes: 48 additions & 1 deletion parachain/pallets/outbound-queue/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use frame_support::{
use codec::Encode;
use snowbridge_core::{
outbound::{Command, SendError, SendMessage},
ParaId,
pricing::InvalidPricingParameters,
ParaId, PricingParameters, Rewards,
};
use sp_arithmetic::FixedU128;
use sp_core::H256;
Expand Down Expand Up @@ -266,3 +267,49 @@ fn encode_digest_item() {
);
});
}

#[test]
fn validate_messages_with_fees() {
new_tester().execute_with(|| {
let message = mock_message(1000);
let (_, fee) = OutboundQueue::validate(&message).unwrap();
assert_eq!(fee.local, 698000000);
assert_eq!(fee.remote, 2680000000000);
});
}

#[test]
fn test_calculate_fees() {
new_tester().execute_with(|| {
let gas_used: u64 = 250000;
let illegal_price_params: PricingParameters<<Test as Config>::Balance> =
claravanstaden marked this conversation as resolved.
Show resolved Hide resolved
PricingParameters {
exchange_rate: FixedU128::from_rational(1, 400),
fee_per_gas: 10000_u32.into(),
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
};
let fee = OutboundQueue::calculate_fee(gas_used, illegal_price_params);
assert_eq!(fee.local, 698000000);
assert_eq!(fee.remote, 1000000);
});
}

#[test]
fn test_calculate_fees_with_exchange_rate_invalid() {
new_tester().execute_with(|| {
let gas_used: u64 = 250000;
let illegal_price_params: PricingParameters<<Test as Config>::Balance> =
PricingParameters {
exchange_rate: FixedU128::from_rational(1, 1),
fee_per_gas: 1_u32.into(),
rewards: Rewards { local: 1_u32.into(), remote: 1_u32.into() },
};
let fee = OutboundQueue::calculate_fee(gas_used, illegal_price_params.clone());
assert_eq!(fee.local, 698000000);
// Though none zero pricing params the remote fee calculated here is invalid
// which should be avoided
assert_eq!(fee.remote, 0);
// assert the pricing params as illegal as expected
assert_err!(illegal_price_params.validate(), InvalidPricingParameters)
});
}
6 changes: 6 additions & 0 deletions parachain/pallets/system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ pub mod pallet {
Send(SendError),
InvalidTokenTransferFees,
InvalidPricingParameters,
InvalidUpgradeParameters,
}

/// The set of registered agents
Expand Down Expand Up @@ -282,6 +283,11 @@ pub mod pallet {
) -> DispatchResult {
ensure_root(origin)?;

ensure!(
!impl_address.eq(&H160::zero()) && !impl_code_hash.eq(&H256::zero()),
Error::<T>::InvalidUpgradeParameters
);

let initializer_params_hash: Option<H256> =
initializer.as_ref().map(|i| H256::from(blake2_256(i.params.as_ref())));
let command = Command::Upgrade { impl_address, impl_code_hash, initializer };
Expand Down
48 changes: 24 additions & 24 deletions parachain/pallets/system/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
use crate::{mock::*, *};
use frame_support::{assert_noop, assert_ok};
use hex_literal::hex;
use snowbridge_core::{eth, sibling_sovereign_account_raw};
use snowbridge_core::eth;
use sp_core::H256;
use sp_runtime::{AccountId32, DispatchError::BadOrigin, TokenError};
use sp_runtime::{AccountId32, DispatchError::BadOrigin, FixedU128, TokenError};

#[test]
fn create_agent() {
Expand Down Expand Up @@ -87,8 +87,8 @@ fn create_agent_bad_origin() {
fn upgrade_as_root() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
let address: H160 = Default::default();
let code_hash: H256 = Default::default();
let address: H160 = [1_u8; 20].into();
let code_hash: H256 = [1_u8; 32].into();

assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, None));

Expand All @@ -115,8 +115,8 @@ fn upgrade_as_signed_fails() {
fn upgrade_with_params() {
new_test_ext(true).execute_with(|| {
let origin = RuntimeOrigin::root();
let address: H160 = Default::default();
let code_hash: H256 = Default::default();
let address: H160 = [1_u8; 20].into();
let code_hash: H256 = [1_u8; 32].into();
let initializer: Option<Initializer> =
Some(Initializer { params: [0; 256].into(), maximum_required_gas: 10000 });
assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, initializer));
Expand Down Expand Up @@ -202,6 +202,22 @@ fn set_pricing_parameters_invalid() {
);
params = Parameters::get();
params.rewards.remote = sp_core::U256::zero();
assert_noop!(
EthereumSystem::set_pricing_parameters(origin.clone(), params),
yrong marked this conversation as resolved.
Show resolved Hide resolved
Error::<Test>::InvalidPricingParameters
);

// Invalid exchange rate with DOT expensive than Ether
params = Parameters::get();
params.exchange_rate = FixedU128::from_rational(2, 1);
assert_noop!(
EthereumSystem::set_pricing_parameters(origin.clone(), params),
Error::<Test>::InvalidPricingParameters
);

// Invalid gas fee too cheap
params = Parameters::get();
params.fee_per_gas = 1_u128.into();
assert_noop!(
EthereumSystem::set_pricing_parameters(origin, params),
Error::<Test>::InvalidPricingParameters
Expand Down Expand Up @@ -551,22 +567,6 @@ fn force_transfer_native_from_agent_bad_origin() {
// conversions for devops purposes. They need to be removed here and incorporated into a command
// line utility.

#[ignore]
#[test]
fn check_sibling_sovereign_account() {
new_test_ext(true).execute_with(|| {
let para_id = 1001;
let sovereign_account = sibling_sovereign_account::<Test>(para_id.into());
let sovereign_account_raw = sibling_sovereign_account_raw(para_id.into());
println!(
"Sovereign account for parachain {}: {:#?}",
para_id,
hex::encode(sovereign_account.clone())
);
assert_eq!(sovereign_account, sovereign_account_raw.into());
});
}

#[test]
fn charge_fee_for_create_agent() {
new_test_ext(true).execute_with(|| {
Expand Down Expand Up @@ -636,8 +636,8 @@ fn charge_fee_for_upgrade() {
new_test_ext(true).execute_with(|| {
let para_id: u32 = TestParaId::get();
let origin = RuntimeOrigin::root();
let address: H160 = Default::default();
let code_hash: H256 = Default::default();
let address: H160 = [1_u8; 20].into();
let code_hash: H256 = [1_u8; 32].into();
let initializer: Option<Initializer> =
Some(Initializer { params: [0; 256].into(), maximum_required_gas: 10000 });
assert_ok!(EthereumSystem::upgrade(origin, address, code_hash, initializer.clone()));
Expand Down
4 changes: 0 additions & 4 deletions parachain/primitives/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ where
SiblingParaId::from(para_id).into_account_truncating()
}

pub fn sibling_sovereign_account_raw(para_id: ParaId) -> [u8; 32] {
yrong marked this conversation as resolved.
Show resolved Hide resolved
SiblingParaId::from(para_id).into_account_truncating()
}

pub struct AllowSiblingsOnly;
impl Contains<MultiLocation> for AllowSiblingsOnly {
fn contains(location: &MultiLocation) -> bool {
Expand Down
9 changes: 9 additions & 0 deletions parachain/primitives/core/src/outbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,18 @@ pub enum SendError {
Halted,
/// Invalid Channel
InvalidChannel,
/// Message with gas or fees calculated invalid
MessageInvalidGasFees,
}

pub trait GasMeter {
/// All the gas used for submitting a message to Ethereum, minus the cost of dispatching
/// the command within the message
const MAXIMUM_BASE_GAS: u64;

/// Hard limit for the maximum gas allowed among all commands
const MAXIMUM_GAS_CAP: u64;
yrong marked this conversation as resolved.
Show resolved Hide resolved

fn maximum_gas_used_at_most(command: &Command) -> u64 {
Self::MAXIMUM_BASE_GAS + Self::maximum_dispatch_gas_used_at_most(command)
}
Expand All @@ -371,6 +376,8 @@ impl GasMeter for ConstantGasMeter {
// for message verification
const MAXIMUM_BASE_GAS: u64 = 185_000;

const MAXIMUM_GAS_CAP: u64 = 5_000_000;

fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 {
match command {
Command::CreateAgent { .. } => 275_000,
Expand Down Expand Up @@ -405,6 +412,8 @@ impl GasMeter for ConstantGasMeter {
impl GasMeter for () {
const MAXIMUM_BASE_GAS: u64 = 1;

const MAXIMUM_GAS_CAP: u64 = 100;

fn maximum_dispatch_gas_used_at_most(_: &Command) -> u64 {
1
}
Expand Down
11 changes: 7 additions & 4 deletions parachain/primitives/core/src/pricing.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::gwei;
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_arithmetic::traits::{BaseArithmetic, Unsigned, Zero};
use sp_arithmetic::traits::{BaseArithmetic, Unsigned};
use sp_core::U256;
use sp_runtime::{FixedU128, RuntimeDebug};
use sp_std::prelude::*;
Expand All @@ -23,18 +24,20 @@ pub struct Rewards<Balance> {
pub remote: U256,
}

#[derive(RuntimeDebug)]
#[derive(Clone, PartialEq, RuntimeDebug)]
yrong marked this conversation as resolved.
Show resolved Hide resolved
pub struct InvalidPricingParameters;

impl<Balance> PricingParameters<Balance>
where
Balance: BaseArithmetic + Unsigned + Copy,
{
pub fn validate(&self) -> Result<(), InvalidPricingParameters> {
if self.exchange_rate == FixedU128::zero() {
if self.exchange_rate < FixedU128::from_rational(1, 10000) ||
self.exchange_rate > FixedU128::from_rational(1, 1)
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not to change the validation logic. I also thought about this approach, but ultimately decided it wasn't worth it.

  1. If the exchange rate does actually mistakenly get set to 1/9999 (slightly less than the limit), then that practically still ends up blocking the bridge anyway.
  2. The upper limit is arbitrary, and if the real-world exchange rate breaches the limit (quite possible), then a runtime upgrade would be required to fix the validation code.

Copy link
Contributor Author

@yrong yrong Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in f43fce3

My only concern is #1109 (comment) but can be avoided by double check before sending the governance call so should be fine.

return Err(InvalidPricingParameters)
}
if self.fee_per_gas == U256::zero() {
if self.fee_per_gas < gwei(1) || self.fee_per_gas > gwei(1000) {
yrong marked this conversation as resolved.
Show resolved Hide resolved
return Err(InvalidPricingParameters)
}
if self.rewards.local.is_zero() {
Expand Down
2 changes: 1 addition & 1 deletion parachain/primitives/router/src/outbound/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ where
// validate the message
let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}");
SendError::Unroutable
SendError::Fees
claravanstaden marked this conversation as resolved.
Show resolved Hide resolved
})?;

// convert fee to MultiAsset
Expand Down
5 changes: 4 additions & 1 deletion parachain/runtime/runtime-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ workspace = true

[dependencies]
log = { version = "0.4.20", default-features = false }

codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
frame-support = { path = "../../../polkadot-sdk/substrate/frame/support", default-features = false }
frame-system = { path = "../../../polkadot-sdk/substrate/frame/system", default-features = false }
sp-std = { path = "../../../polkadot-sdk/substrate/primitives/std", default-features = false }
sp-arithmetic = { path = "../../../polkadot-sdk/substrate/primitives/arithmetic", default-features = false }
xcm = { package = "staging-xcm", path = "../../../polkadot-sdk/polkadot/xcm", default-features = false }
xcm-builder = { package = "staging-xcm-builder", path = "../../../polkadot-sdk/polkadot/xcm/xcm-builder", default-features = false }
Expand All @@ -28,11 +29,13 @@ snowbridge-core = { path = "../../primitives/core", default-features = false }
[features]
default = ["std"]
std = [
"codec/std",
"frame-support/std",
"frame-system/std",
"log/std",
"snowbridge-core/std",
"sp-arithmetic/std",
"sp-std/std",
"xcm-builder/std",
"xcm-executor/std",
"xcm/std",
Expand Down
Loading
Loading