From 0fc92e31fc352da46c788bf34a24e807a12f095e Mon Sep 17 00:00:00 2001 From: Sander Bosma Date: Mon, 10 Oct 2022 11:48:16 +0200 Subject: [PATCH] feat: limit xcm transfers --- Cargo.lock | 24 ++ crates/currency/src/lib.rs | 4 +- crates/xcm-limit/Cargo.toml | 51 ++++ crates/xcm-limit/src/default_weights.rs | 64 +++++ crates/xcm-limit/src/lib.rs | 250 ++++++++++++++++++ crates/xcm-limit/src/mock.rs | 87 ++++++ parachain/runtime/testnet-kintsugi/Cargo.toml | 2 + parachain/runtime/testnet-kintsugi/src/lib.rs | 1 + .../testnet-kintsugi/src/xcm_config.rs | 42 ++- 9 files changed, 515 insertions(+), 10 deletions(-) create mode 100644 crates/xcm-limit/Cargo.toml create mode 100644 crates/xcm-limit/src/default_weights.rs create mode 100644 crates/xcm-limit/src/lib.rs create mode 100644 crates/xcm-limit/src/mock.rs diff --git a/Cargo.lock b/Cargo.lock index a0869bca3c..b4c3bd7a4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12233,6 +12233,7 @@ dependencies = [ "xcm", "xcm-builder", "xcm-executor", + "xcm-limit", ] [[package]] @@ -13580,6 +13581,29 @@ dependencies = [ "xcm", ] +[[package]] +name = "xcm-limit" +version = "1.2.0" +dependencies = [ + "fixed-hash", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "visibility", + "xcm", + "xcm-executor", +] + [[package]] name = "xcm-procedural" version = "0.1.0" diff --git a/crates/currency/src/lib.rs b/crates/currency/src/lib.rs index 2eb546c431..2845f557a5 100644 --- a/crates/currency/src/lib.rs +++ b/crates/currency/src/lib.rs @@ -42,7 +42,9 @@ pub mod pallet { /// ## Configuration /// The pallet's configuration trait. #[pallet::config] - pub trait Config: frame_system::Config + orml_tokens::Config> { + pub trait Config: + frame_system::Config + orml_tokens::Config, CurrencyId = primitives::CurrencyId> + { type UnsignedFixedPoint: FixedPointNumber> + TruncateFixedPointToInt + Encode diff --git a/crates/xcm-limit/Cargo.toml b/crates/xcm-limit/Cargo.toml new file mode 100644 index 0000000000..cf518f0d9d --- /dev/null +++ b/crates/xcm-limit/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "xcm-limit" +version = "1.2.0" +authors = ["Interlay Ltd"] +edition = "2021" + +[dependencies] +serde = { version = "1.0.130", default-features = false, features = ["derive"], optional = true } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "max-encoded-len"] } +scale-info = { version = "2.0.0", default-features = false, features = ["derive"] } + +fixed-hash = { version = "0.7.0", default-features = false, features = ["byteorder"] } +log = { version = "0.4.14", default-features = false } + +visibility = { version = "0.0.1", optional = true } + +# Substrate dependencies +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false } +sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false } + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false, optional = true } +pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24", default-features = false } + +xcm = { git = "https://github.com/paritytech/polkadot", default-features = false , branch = "release-v0.9.26" } +xcm-executor = { git = "https://github.com/paritytech/polkadot", default-features = false , branch = "release-v0.9.26" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", + + "xcm/std", + "xcm-executor/std" +] \ No newline at end of file diff --git a/crates/xcm-limit/src/default_weights.rs b/crates/xcm-limit/src/default_weights.rs new file mode 100644 index 0000000000..6a4afcda81 --- /dev/null +++ b/crates/xcm-limit/src/default_weights.rs @@ -0,0 +1,64 @@ +//! Autogenerated weights for clients-info +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2021-12-13, STEPS: `100`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// target/release/interbtc-standalone +// benchmark +// --chain +// dev +// --execution=wasm +// --wasm-execution=compiled +// --pallet +// clients-info +// --extrinsic +// * +// --steps +// 100 +// --repeat +// 10 +// --output +// crates/clients-info/src/default_weights.rs +// --template +// .deploy/weight-template.hbs + + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for vault_registry. +pub trait WeightInfo { + fn set_current_client_release() -> Weight; + fn set_pending_client_release() -> Weight; +} + +/// Weights for vault_registry using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn set_current_client_release() -> Weight { + (4_130_000 as Weight) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + + fn set_pending_client_release() -> Weight { + (4_130_000 as Weight) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn set_current_client_release() -> Weight { + (4_130_000 as Weight) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + + fn set_pending_client_release() -> Weight { + (4_130_000 as Weight) + } +} diff --git a/crates/xcm-limit/src/lib.rs b/crates/xcm-limit/src/lib.rs new file mode 100644 index 0000000000..38ba29b781 --- /dev/null +++ b/crates/xcm-limit/src/lib.rs @@ -0,0 +1,250 @@ +//! # ClientsInfo Module +//! Stores information about clients that comprise the network, such as vaults and oracles. + +// #![deny(warnings)] +#![cfg_attr(test, feature(proc_macro_hygiene))] +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::traits::Zero; + +mod default_weights; + +pub use default_weights::WeightInfo; + +use frame_support::{dispatch::DispatchResult, pallet_prelude::*, traits::Get, transactional}; +use sp_runtime::{ + traits::{Convert, Saturating}, + SaturatedConversion, +}; +use sp_std::{fmt::Debug, vec::Vec}; +use xcm::{ + latest::{Error as XcmError, Instruction::*, MultiAsset, MultiLocation}, + v2::Fungibility, +}; +use xcm_executor::{ + traits::{ShouldExecute, TransactAsset}, + Assets, +}; + +#[cfg(test)] +mod mock; + +#[derive(Encode, Decode, Eq, PartialEq, Clone, Default, TypeInfo, Debug)] +pub struct ClientRelease { + /// URI to the client release binary. + pub uri: Vec, + /// The SHA256 checksum of the client binary. + pub checksum: Hash, +} + +pub use pallet::*; + +pub trait LocationCategorizer { + fn is_local_account(location: MultiLocation) -> bool; +} + +#[frame_support::pallet] +pub mod pallet { + use crate::*; + + use codec::FullCodec; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::{AtLeast32BitUnsigned, Saturating}; + + #[pallet::pallet] + #[pallet::generate_store(trait Store)] + #[pallet::without_storage_info] // ClientRelease struct contains vec which doesn't implement MaxEncodedLen + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + + Into<::Event> + + IsType<::Event>; + + /// Weight information for the extrinsics in this module. + type WeightInfo: WeightInfo; + + type Transactor: TransactAsset; + + type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord; + + type CurrencyIdConvert: Convert>; + + type Balance: AtLeast32BitUnsigned + + Member + + FullCodec + + Copy + + Saturating + + Default + + Debug + + TypeInfo + + MaxEncodedLen; + + type LocationCategorizer: LocationCategorizer; + + /// The interval at which the budget is reset. + #[pallet::constant] + type Period: Get; + } + + #[pallet::call] + impl Pallet {} + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event {} + + #[pallet::error] + pub enum Error {} + + /// The total balance sent to other chains since the last checkpoint. + #[pallet::storage] + pub(super) type TotalOutbound = StorageMap<_, Blake2_128Concat, T::CurrencyId, T::Balance, ValueQuery>; + + /// The total balance received other chains since the last checkpoint. + #[pallet::storage] + pub(super) type TotalInbound = StorageMap<_, Blake2_128Concat, T::CurrencyId, T::Balance, ValueQuery>; + + /// The total balance allowed to be sent to other chains per interval. + #[pallet::storage] + pub(super) type OutboundLimit = StorageMap<_, Blake2_128Concat, T::CurrencyId, T::Balance, ValueQuery>; + + /// The total balance allowed to be received from other chains per interval. + // todo: does having 2 separate limits make sense? Maybe 1 is enough + #[pallet::storage] + pub(super) type InboundLimit = StorageMap<_, Blake2_128Concat, T::CurrencyId, T::Balance, ValueQuery>; + + #[pallet::hooks] + impl Hooks for Pallet { + fn on_initialize(n: T::BlockNumber) -> Weight { + let _ = Self::begin_block(n); + 0 + } + } +} + +#[cfg_attr(test, mockable)] +impl Pallet { + fn begin_block(height: T::BlockNumber) -> DispatchResult { + if (height % T::Period::get()).is_zero() { + let _ = >::clear(u32::max_value(), None); + let _ = >::clear(u32::max_value(), None); + } + Ok(()) + } + + fn extract_amount(asset: &MultiAsset) -> Result<(T::Balance, T::CurrencyId), XcmError> { + let amount = match asset { + &MultiAsset { + fun: Fungibility::Fungible(x), + .. + } => x.saturated_into(), + _ => return Err(XcmError::FailedToTransactAsset("FailedToMatchFungible")), + }; + let currency_id = T::CurrencyIdConvert::convert(asset.clone()) + .ok_or(XcmError::FailedToTransactAsset("CurrencyIdConversionFailed"))?; + Ok((amount, currency_id)) + } + + fn on_deposit(asset: &MultiAsset, location: &MultiLocation) -> Result<(), XcmError> { + let (amount, currency_id) = Self::extract_amount(asset)?; + if T::LocationCategorizer::is_local_account(location.clone()) { + >::mutate(currency_id, |x| { + x.saturating_accrue(amount); + Ok(()) + }) + } else { + >::mutate(currency_id, |x| { + x.saturating_accrue(amount); + Ok(()) + }) + } + } +} +impl TransactAsset for Pallet { + fn deposit_asset(asset: &MultiAsset, location: &MultiLocation) -> Result<(), XcmError> { + Self::on_deposit(asset, location)?; + T::Transactor::deposit_asset(asset, location) + } + + fn withdraw_asset(asset: &MultiAsset, location: &MultiLocation) -> Result { + T::Transactor::withdraw_asset(asset, location) + } + + fn transfer_asset(asset: &MultiAsset, from: &MultiLocation, to: &MultiLocation) -> Result { + Self::on_deposit(asset, to)?; + T::Transactor::transfer_asset(asset, from, to) + } +} + +pub struct And(PhantomData<(T, U)>); + +impl ShouldExecute for And { + fn should_execute( + origin: &MultiLocation, + message: &mut xcm::v2::Xcm, + max_weight: frame_support::weights::Weight, + weight_credit: &mut frame_support::weights::Weight, + ) -> Result<(), ()> { + T::should_execute(origin, message, max_weight, weight_credit)?; + U::should_execute(origin, message, max_weight, weight_credit)?; + // only if both returned ok, we return ok + Ok(()) + } +} + +impl ShouldExecute for Pallet { + fn should_execute( + origin: &MultiLocation, + message: &mut xcm::v2::Xcm, + _max_weight: frame_support::weights::Weight, + _weight_credit: &mut frame_support::weights::Weight, + ) -> Result<(), ()> { + let first_instruction = match message.0.iter().next() { + Some(x) => x, + None => return Ok(()), // not hitting our limit filter + }; + + let is_outbound = T::LocationCategorizer::is_local_account(origin.clone()); + + if is_outbound { + // xtokens executes the following on outbound transfers: + // transfer_to_reserve: [WithdrawAsset, InitiateReserveWithdraw] + // transfer_self_reserve_asset: TransferReserveAsset + // transfer_to_non_reserve: [WithdrawAsset, InitiateReserveWithdraw] + match first_instruction { + WithdrawAsset(assets) | TransferReserveAsset { assets, .. } => { + for asset in assets.inner() { + let (amount, currency_id) = Self::extract_amount(asset).map_err(|_| ())?; + let limit = >::get(currency_id); + let total_outbound = >::get(currency_id); + if total_outbound.saturating_add(amount) > limit { + return Err(()); // disallow! + } + } + } + _ => {} + } + } else { + match first_instruction { + ReceiveTeleportedAsset(assets) | WithdrawAsset(assets) | ClaimAsset { assets, .. } => { + for asset in assets.inner() { + let (amount, currency_id) = Self::extract_amount(asset).map_err(|_| ())?; + let limit = >::get(currency_id); + let total_inbound = >::get(currency_id); + if total_inbound.saturating_add(amount) > limit { + return Err(()); // disallow! + } + } + } + _ => {} + } + } + Ok(()) + } +} diff --git a/crates/xcm-limit/src/mock.rs b/crates/xcm-limit/src/mock.rs new file mode 100644 index 0000000000..ed31ef2164 --- /dev/null +++ b/crates/xcm-limit/src/mock.rs @@ -0,0 +1,87 @@ +use crate as clients_info; +use crate::Config; +use frame_support::{parameter_types, traits::Everything}; +use sp_core::H256; +use sp_runtime::{ + generic::Header as GenericHeader, + traits::{BlakeTwo256, IdentityLookup}, +}; + +type Header = GenericHeader; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Config, Event}, + ClientsInfo: clients_info::{Pallet, Call, Storage, Event} + } +); + +pub type AccountId = u64; +pub type BlockNumber = u64; +pub type Index = u64; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = Index; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +pub type TestEvent = Event; + +impl Config for Test { + type Event = TestEvent; + type WeightInfo = (); +} + +pub struct ExtBuilder; + +impl ExtBuilder { + pub fn build() -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + storage.into() + } +} + +pub fn run_test(test: T) +where + T: FnOnce(), +{ + ExtBuilder::build().execute_with(|| { + System::set_block_number(0); + test(); + }); +} diff --git a/parachain/runtime/testnet-kintsugi/Cargo.toml b/parachain/runtime/testnet-kintsugi/Cargo.toml index 44e5f1840f..3756e286c4 100644 --- a/parachain/runtime/testnet-kintsugi/Cargo.toml +++ b/parachain/runtime/testnet-kintsugi/Cargo.toml @@ -94,6 +94,7 @@ annuity = { path = "../../../crates/annuity", default-features = false } supply = { path = "../../../crates/supply", default-features = false } collator-selection = { path = "../../../crates/collator-selection", default-features = false } clients-info = { path = "../../../crates/clients-info", default-features = false } +xcm-limit = { path = "../../../crates/xcm-limit", default-features = false } primitives = { package = "interbtc-primitives", path = "../../../primitives", default-features = false } @@ -208,6 +209,7 @@ std = [ "annuity/std", "supply/std", "collator-selection/std", + "xcm-limit/std", "primitives/std", diff --git a/parachain/runtime/testnet-kintsugi/src/lib.rs b/parachain/runtime/testnet-kintsugi/src/lib.rs index 61a554aa23..e06c343718 100644 --- a/parachain/runtime/testnet-kintsugi/src/lib.rs +++ b/parachain/runtime/testnet-kintsugi/src/lib.rs @@ -1082,6 +1082,7 @@ construct_runtime! { XTokens: orml_xtokens::{Pallet, Storage, Call, Event} = 94, UnknownTokens: orml_unknown_tokens::{Pallet, Storage, Event} = 95, + XcmLimit: xcm_limit::{Pallet, Storage, Event} = 97, } } diff --git a/parachain/runtime/testnet-kintsugi/src/xcm_config.rs b/parachain/runtime/testnet-kintsugi/src/xcm_config.rs index 8951c814f6..fb4ba48702 100644 --- a/parachain/runtime/testnet-kintsugi/src/xcm_config.rs +++ b/parachain/runtime/testnet-kintsugi/src/xcm_config.rs @@ -14,8 +14,7 @@ use xcm_builder::{ RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeRevenue, TakeWeightCredit, }; -use xcm_executor::{Config, XcmExecutor}; - +use xcm_executor::{traits::Convert as _, Config, XcmExecutor}; parameter_types! { pub const ParentLocation: MultiLocation = MultiLocation::parent(); pub const ParentNetwork: NetworkId = NetworkId::Kusama; @@ -59,12 +58,15 @@ parameter_types! { pub UnitWeightCost: Weight = 200_000_000; } -pub type Barrier = ( - TakeWeightCredit, - AllowTopLevelPaidExecutionFrom, - AllowKnownQueryResponses, - AllowSubscriptionsFrom, -); // required for others to keep track of our xcm version +pub type Barrier = xcm_limit::And< + ( + TakeWeightCredit, + AllowTopLevelPaidExecutionFrom, + AllowKnownQueryResponses, + AllowSubscriptionsFrom, // required for others to keep track of our xcm version + ), + XcmLimit, +>; parameter_types! { pub const MaxInstructions: u32 = 100; @@ -161,7 +163,7 @@ impl Config for XcmConfig { type Call = Call; type XcmSender = XcmRouter; // How to withdraw and deposit an asset. - type AssetTransactor = LocalAssetTransactor; + type AssetTransactor = XcmLimit; type OriginConverter = XcmOriginToTransactDispatchOrigin; type IsReserve = MultiNativeAsset; type IsTeleporter = NativeAsset; // <- should be enough to allow teleportation @@ -368,3 +370,25 @@ impl orml_xtokens::Config for Runtime { type MultiLocationsFilter = Everything; type ReserveProvider = AbsoluteReserveProvider; } + +parameter_types! { + pub const XcmLimitPeriod: BlockNumber = DAYS; +} +impl xcm_limit::Config for Runtime { + type Event = Event; + type WeightInfo = (); + type Transactor = LocalAssetTransactor; + type Balance = Balance; + type CurrencyId = CurrencyId; + type CurrencyIdConvert = CurrencyIdConvert; + type LocationCategorizer = LocationCategorizer; + type Period = XcmLimitPeriod; +} + +pub struct LocationCategorizer; + +impl xcm_limit::LocationCategorizer for LocationCategorizer { + fn is_local_account(location: MultiLocation) -> bool { + AccountId32Aliases::::convert(location).is_ok() + } +}