diff --git a/runtime/kusama/src/lib.rs b/runtime/kusama/src/lib.rs index d17e3f4f358f..c1ffe627ebbf 100644 --- a/runtime/kusama/src/lib.rs +++ b/runtime/kusama/src/lib.rs @@ -1116,6 +1116,8 @@ parameter_types! { /// Our XCM location ancestry - i.e. what, if anything, `Parent` means evaluated in our context. Since /// Kusama is a top-level relay-chain, there is no ancestry. pub const Ancestry: MultiLocation = MultiLocation::Null; + /// The check account, which holds any native assets that have been teleported out and not back in (yet). + pub CheckAccount: AccountId = XcmPallet::check_account(); } /// The canonical means of converting a `MultiLocation` into an `AccountId`, used when we want to determine @@ -1141,6 +1143,8 @@ pub type LocalAssetTransactor = SovereignAccountOf, // Our chain's account ID type (we can't get away without mentioning it explicitly): AccountId, + // We track our teleports in/out to keep total issuance correct. + CheckAccount, >; /// The means that we convert an the XCM message origin location into a local dispatch origin. diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 1965d4006ea9..583e43c05c06 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -557,6 +557,7 @@ parameter_types! { pub const RocLocation: MultiLocation = MultiLocation::Null; pub const RococoNetwork: NetworkId = NetworkId::Polkadot; pub const Ancestry: MultiLocation = MultiLocation::Null; + pub CheckAccount: AccountId = XcmPallet::check_account(); } pub type SovereignAccountOf = ( @@ -574,6 +575,8 @@ pub type LocalAssetTransactor = SovereignAccountOf, // Our chain's account ID type (we can't get away without mentioning it explicitly): AccountId, + // It's a native asset so we keep track of the teleports to maintain total issuance. + CheckAccount, >; type LocalOriginConverter = ( diff --git a/runtime/westend/src/lib.rs b/runtime/westend/src/lib.rs index 05cad15a5e40..ff03db6cb6ae 100644 --- a/runtime/westend/src/lib.rs +++ b/runtime/westend/src/lib.rs @@ -796,6 +796,7 @@ parameter_types! { pub const WndLocation: MultiLocation = MultiLocation::Null; pub const Ancestry: MultiLocation = MultiLocation::Null; pub WestendNetwork: NetworkId = NetworkId::Named(b"Westend".to_vec()); + pub CheckAccount: AccountId = XcmPallet::check_account(); } pub type LocationConverter = ( @@ -813,6 +814,8 @@ pub type LocalAssetTransactor = LocationConverter, // Our chain's account ID type (we can't get away without mentioning it explicitly): AccountId, + // It's a native asset so we keep track of the teleports to maintain total issuance. + CheckAccount, >; type LocalOriginConverter = ( diff --git a/xcm/pallet-xcm/src/lib.rs b/xcm/pallet-xcm/src/lib.rs index b15a6f491fea..febc4b395492 100644 --- a/xcm/pallet-xcm/src/lib.rs +++ b/xcm/pallet-xcm/src/lib.rs @@ -26,6 +26,7 @@ use sp_runtime::{RuntimeDebug, traits::BadOrigin}; use frame_support::traits::{EnsureOrigin, OriginTrait, Filter, Get, Contains}; pub use pallet::*; +use frame_support::PalletId; #[frame_support::pallet] pub mod pallet { @@ -33,6 +34,7 @@ pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use xcm_executor::traits::WeightBounds; + use sp_runtime::traits::AccountIdConversion; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] @@ -198,6 +200,11 @@ pub mod pallet { }; T::XcmRouter::send_xcm(dest, message) } + + pub fn check_account() -> T::AccountId { + const ID: PalletId = PalletId(*b"py/xcmch"); + AccountIdConversion::::into_account(&ID) + } } } diff --git a/xcm/src/v0/traits.rs b/xcm/src/v0/traits.rs index 47613a7ec007..1202e0f4b9b9 100644 --- a/xcm/src/v0/traits.rs +++ b/xcm/src/v0/traits.rs @@ -77,6 +77,8 @@ pub enum Error { LocationCannotHold, /// The assets given to purchase weight is are insufficient for the weight desired. TooExpensive, + /// The given asset is not handled. + AssetNotFound, } impl From<()> for Error { diff --git a/xcm/xcm-builder/src/currency_adapter.rs b/xcm/xcm-builder/src/currency_adapter.rs index 4b08398a2dfb..cc1fb29d50e2 100644 --- a/xcm/xcm-builder/src/currency_adapter.rs +++ b/xcm/xcm-builder/src/currency_adapter.rs @@ -16,8 +16,8 @@ use sp_std::{result, convert::TryInto, marker::PhantomData}; use xcm::v0::{Error as XcmError, Result, MultiAsset, MultiLocation}; -use sp_runtime::traits::SaturatedConversion; -use frame_support::traits::{ExistenceRequirement::AllowDeath, WithdrawReasons}; +use sp_runtime::traits::{SaturatedConversion, CheckedSub}; +use frame_support::traits::{ExistenceRequirement::AllowDeath, WithdrawReasons, Get}; use xcm_executor::traits::{MatchesFungible, Convert, TransactAsset}; use xcm_executor::Assets; @@ -43,8 +43,8 @@ impl From for XcmError { } } -pub struct CurrencyAdapter( - PhantomData<(Currency, Matcher, AccountIdConverter, AccountId)> +pub struct CurrencyAdapter( + PhantomData<(Currency, Matcher, AccountIdConverter, AccountId, CheckedAccount)> ); impl< @@ -52,7 +52,38 @@ impl< AccountIdConverter: Convert, Currency: frame_support::traits::Currency, AccountId: Clone, // can't get away without it since Currency is generic over it. -> TransactAsset for CurrencyAdapter { + CheckedAccount: Get>, +> TransactAsset for CurrencyAdapter { + fn can_check_in(_origin: &MultiLocation, what: &MultiAsset) -> Result { + // Check we handle this asset. + let amount: Currency::Balance = Matcher::matches_fungible(what) + .ok_or(Error::AssetNotFound)?; + if let Some(checked_account) = CheckedAccount::get() { + let new_balance = Currency::free_balance(&checked_account) + .checked_sub(&amount) + .ok_or(XcmError::NotWithdrawable)?; + Currency::ensure_can_withdraw(&checked_account, amount, WithdrawReasons::TRANSFER, new_balance) + .map_err(|_| XcmError::NotWithdrawable)?; + } + Ok(()) + } + + fn check_in(_origin: &MultiLocation, what: &MultiAsset) { + if let Some(amount) = Matcher::matches_fungible(what) { + if let Some(checked_account) = CheckedAccount::get() { + let ok = Currency::withdraw(&checked_account, amount, WithdrawReasons::TRANSFER, AllowDeath).is_ok(); + debug_assert!(ok, "`can_check_in` must have returned `true` immediately prior; qed"); + } + } + } + + fn check_out(_dest: &MultiLocation, what: &MultiAsset) { + if let Some(amount) = Matcher::matches_fungible(what) { + if let Some(checked_account) = CheckedAccount::get() { + Currency::deposit_creating(&checked_account, amount); + } + } + } fn deposit_asset(what: &MultiAsset, who: &MultiLocation) -> Result { // Check we handle this asset. diff --git a/xcm/xcm-builder/src/fungibles_adapter.rs b/xcm/xcm-builder/src/fungibles_adapter.rs index adb865ea7086..4f2b0ce0e089 100644 --- a/xcm/xcm-builder/src/fungibles_adapter.rs +++ b/xcm/xcm-builder/src/fungibles_adapter.rs @@ -16,7 +16,7 @@ use sp_std::{prelude::*, result, marker::PhantomData, borrow::Borrow}; use xcm::v0::{Error as XcmError, Result, MultiAsset, MultiLocation, Junction}; -use frame_support::traits::{Get, tokens::fungibles}; +use frame_support::traits::{Get, tokens::fungibles, Contains}; use xcm_executor::traits::{TransactAsset, Convert}; /// Asset transaction errors. @@ -160,15 +160,49 @@ impl< } } -pub struct FungiblesMutateAdapter( - PhantomData<(Assets, Matcher, AccountIdConverter, AccountId)> +pub struct FungiblesMutateAdapter( + PhantomData<(Assets, Matcher, AccountIdConverter, AccountId, CheckAsset, CheckingAccount)> ); impl< Assets: fungibles::Mutate, Matcher: MatchesFungibles, AccountIdConverter: Convert, AccountId: Clone, // can't get away without it since Currency is generic over it. -> TransactAsset for FungiblesMutateAdapter { + CheckAsset: Contains, + CheckingAccount: Get, +> TransactAsset for FungiblesMutateAdapter { + fn can_check_in(_origin: &MultiLocation, what: &MultiAsset) -> Result { + // Check we handle this asset. + let (asset_id, amount) = Matcher::matches_fungibles(what)?; + if CheckAsset::contains(&asset_id) { + // This is an asset whose teleports we track. + let checking_account = CheckingAccount::get(); + Assets::can_withdraw(asset_id, &checking_account, amount) + .into_result() + .map_err(|_| XcmError::NotWithdrawable)?; + } + Ok(()) + } + + fn check_in(_origin: &MultiLocation, what: &MultiAsset) { + if let Ok((asset_id, amount)) = Matcher::matches_fungibles(what) { + if CheckAsset::contains(&asset_id) { + let checking_account = CheckingAccount::get(); + let ok = Assets::burn_from(asset_id, &checking_account, amount).is_ok(); + debug_assert!(ok, "`can_check_in` must have returned `true` immediately prior; qed"); + } + } + } + + fn check_out(_dest: &MultiLocation, what: &MultiAsset) { + if let Ok((asset_id, amount)) = Matcher::matches_fungibles(what) { + if CheckAsset::contains(&asset_id) { + let checking_account = CheckingAccount::get(); + let ok = Assets::mint_into(asset_id, &checking_account, amount).is_ok(); + debug_assert!(ok, "`mint_into` cannot generally fail; qed"); + } + } + } fn deposit_asset(what: &MultiAsset, who: &MultiLocation) -> Result { // Check we handle this asset. @@ -193,25 +227,43 @@ impl< } } -pub struct FungiblesAdapter( - PhantomData<(Assets, Matcher, AccountIdConverter, AccountId)> +pub struct FungiblesAdapter( + PhantomData<(Assets, Matcher, AccountIdConverter, AccountId, CheckAsset, CheckingAccount)> ); impl< Assets: fungibles::Mutate + fungibles::Transfer, Matcher: MatchesFungibles, AccountIdConverter: Convert, AccountId: Clone, // can't get away without it since Currency is generic over it. -> TransactAsset for FungiblesAdapter { + CheckAsset: Contains, + CheckingAccount: Get, +> TransactAsset for FungiblesAdapter { + fn can_check_in(origin: &MultiLocation, what: &MultiAsset) -> Result { + FungiblesMutateAdapter:: + ::can_check_in(origin, what) + } + + fn check_in(origin: &MultiLocation, what: &MultiAsset) { + FungiblesMutateAdapter:: + ::check_in(origin, what) + } + + fn check_out(dest: &MultiLocation, what: &MultiAsset) { + FungiblesMutateAdapter:: + ::check_out(dest, what) + } fn deposit_asset(what: &MultiAsset, who: &MultiLocation) -> Result { - FungiblesMutateAdapter::::deposit_asset(what, who) + FungiblesMutateAdapter:: + ::deposit_asset(what, who) } fn withdraw_asset( what: &MultiAsset, who: &MultiLocation ) -> result::Result { - FungiblesMutateAdapter::::withdraw_asset(what, who) + FungiblesMutateAdapter:: + ::withdraw_asset(what, who) } fn transfer_asset( diff --git a/xcm/xcm-executor/src/lib.rs b/xcm/xcm-executor/src/lib.rs index c99bf6d05fc4..21dd62cc19ae 100644 --- a/xcm/xcm-executor/src/lib.rs +++ b/xcm/xcm-executor/src/lib.rs @@ -154,6 +154,13 @@ impl XcmExecutor { // We only trust the origin to send us assets that they identify as their // sovereign assets. ensure!(Config::IsTeleporter::filter_asset_location(asset, &origin), XcmError::UntrustedTeleportLocation); + // We should check that the asset can actually be teleported in (for this to be in error, there + // would need to be an accounting violation by one of the trusted chains, so it's unlikely, but we + // don't want to punish a possibly innocent chain/user). + Config::AssetTransactor::can_check_in(&origin, asset)?; + } + for asset in assets.iter() { + Config::AssetTransactor::check_in(&origin, asset); } Some((Assets::from(assets), effects)) } @@ -238,6 +245,9 @@ impl XcmExecutor { Config::XcmSender::send_xcm(reserve, Xcm::WithdrawAsset { assets, effects })?; } Order::InitiateTeleport { assets, dest, effects} => { + for asset in assets.iter() { + Config::AssetTransactor::check_out(&origin, asset); + } let assets = Self::reanchored(holding.saturating_take(assets), &dest); Config::XcmSender::send_xcm(dest, Xcm::TeleportAsset { assets, effects })?; } diff --git a/xcm/xcm-executor/src/traits/transact_asset.rs b/xcm/xcm-executor/src/traits/transact_asset.rs index 7e699425e3ee..7b0590098e8f 100644 --- a/xcm/xcm-executor/src/traits/transact_asset.rs +++ b/xcm/xcm-executor/src/traits/transact_asset.rs @@ -24,6 +24,40 @@ use crate::Assets; /// account locations such as a `MultiLocation::X1(Junction::Parachain)`. Different chains may handle them in /// different ways. pub trait TransactAsset { + /// Ensure that `check_in` will result in `Ok`. + /// + /// When composed as a tuple, all type-items are called and at least one must result in `Ok`. + fn can_check_in(_origin: &MultiLocation, _what: &MultiAsset) -> XcmResult { + Err(XcmError::Unimplemented) + } + + /// An asset has been teleported in from the given origin. This should do whatever housekeeping is needed. + /// + /// NOTE: This will make only a best-effort at bookkeeping. The caller should ensure that `can_check_in` has + /// returned with `Ok` in order to guarantee that this operation proceeds properly. + /// + /// Implementation note: In general this will do one of two things: On chains where the asset is native, + /// it will reduce the assets from a special "teleported" account so that a) total-issuance is preserved; + /// and b) to ensure that no more assets can be teleported in than were teleported out overall (this should + /// not be needed if the teleporting chains are to be trusted, but better to be safe than sorry). On chains + /// where the asset is not native then it will generally just be a no-op. + /// + /// When composed as a tuple, all type-items are called. It is up to the implementor that there exists no + /// value for `_what` which can cause side-effects for more than one of the type-items. + fn check_in(_origin: &MultiLocation, _what: &MultiAsset) {} + + /// An asset has been teleported out to the given destination. This should do whatever housekeeping is needed. + /// + /// Implementation note: In general this will do one of two things: On chains where the asset is native, + /// it will increase the assets in a special "teleported" account so that a) total-issuance is preserved; and + /// b) to ensure that no more assets can be teleported in than were teleported out overall (this should not + /// be needed if the teleporting chains are to be trusted, but better to be safe than sorry). On chains where + /// the asset is not native then it will generally just be a no-op. + /// + /// When composed as a tuple, all type-items are called. It is up to the implementor that there exists no + /// value for `_what` which can cause side-effects for more than one of the type-items. + fn check_out(_origin: &MultiLocation, _what: &MultiAsset) {} + /// Deposit the `what` asset into the account of `who`. /// /// Implementations should return `XcmError::FailedToTransactAsset` if deposit failed. @@ -64,6 +98,25 @@ pub trait TransactAsset { #[impl_trait_for_tuples::impl_for_tuples(30)] impl TransactAsset for Tuple { + fn can_check_in(origin: &MultiLocation, what: &MultiAsset) -> XcmResult { + for_tuples!( #( + match Tuple::can_check_in(origin, what) { + Err(XcmError::AssetNotFound) => (), + r => return r, + } + )* ); + Err(XcmError::AssetNotFound) + } + fn check_in(origin: &MultiLocation, what: &MultiAsset) { + for_tuples!( #( + Tuple::check_in(origin, what); + )* ); + } + fn check_out(dest: &MultiLocation, what: &MultiAsset) { + for_tuples!( #( + Tuple::check_out(dest, what); + )* ); + } fn deposit_asset(what: &MultiAsset, who: &MultiLocation) -> XcmResult { for_tuples!( #( match Tuple::deposit_asset(what, who) { o @ Ok(_) => return o, _ => () } @@ -83,4 +136,3 @@ impl TransactAsset for Tuple { Err(XcmError::Unimplemented) } } -