XCM: Allow reclaim of assets dropped from holding (#3727)

* XCM: Introduce AssetTrap

* Revert reversions

* Remove attempts at weighing and add test

* Less storage use for asset trapping

* Add missing file

* Fixes

* Fixes

* Formatting

* Fixes

* Docs

* Filter types to allow runtimes to dictate which assets/origins should be trapped

* Formatting

* Tests

* Formatting

* Fixes

* Docs
This commit is contained in:
Gavin Wood
2021-08-28 02:09:36 +02:00
committed by GitHub
parent e56efb82d9
commit 636d0b030f
24 changed files with 519 additions and 49 deletions
+1 -1
View File
@@ -1044,7 +1044,7 @@ mod tests {
}
#[test]
fn invalid_attest_transactions_are_recognised() {
fn invalid_attest_transactions_are_recognized() {
new_test_ext().execute_with(|| {
let p = PrevalidateAttests::<Test>::new();
let c = Call::Claims(ClaimsCall::attest(StatementKind::Regular.to_text().to_vec()));
+3 -1
View File
@@ -1296,7 +1296,9 @@ impl xcm_executor::Config for XcmConfig {
type Weigher = FixedWeightBounds<BaseXcmWeight, Call, MaxInstructions>;
// The weight trader piggybacks on the existing transaction-fee conversion logic.
type Trader = UsingComponents<WeightToFee, KsmLocation, AccountId, Balances, ToAuthor<Runtime>>;
type ResponseHandler = ();
type ResponseHandler = XcmPallet;
type AssetTrap = XcmPallet;
type AssetClaims = XcmPallet;
}
parameter_types! {
+3 -1
View File
@@ -669,7 +669,9 @@ impl xcm_executor::Config for XcmConfig {
type Barrier = Barrier;
type Weigher = FixedWeightBounds<BaseXcmWeight, Call, MaxInstructions>;
type Trader = UsingComponents<WeightToFee, RocLocation, AccountId, Balances, ToAuthor<Runtime>>;
type ResponseHandler = ();
type ResponseHandler = XcmPallet;
type AssetTrap = XcmPallet;
type AssetClaims = XcmPallet;
}
parameter_types! {
@@ -86,4 +86,6 @@ impl xcm_executor::Config for XcmConfig {
type Weigher = FixedWeightBounds<super::BaseXcmWeight, super::Call, MaxInstructions>;
type Trader = DummyWeightTrader;
type ResponseHandler = super::Xcm;
type AssetTrap = super::Xcm;
type AssetClaims = super::Xcm;
}
+3 -1
View File
@@ -938,7 +938,9 @@ impl xcm_executor::Config for XcmConfig {
type Barrier = Barrier;
type Weigher = FixedWeightBounds<BaseXcmWeight, Call, MaxInstructions>;
type Trader = UsingComponents<WeightToFee, WndLocation, AccountId, Balances, ToAuthor<Runtime>>;
type ResponseHandler = ();
type ResponseHandler = XcmPallet;
type AssetTrap = XcmPallet;
type AssetClaims = XcmPallet;
}
/// Type to convert an `Origin` type value into a `MultiLocation` value which represents an interior location
+2 -1
View File
@@ -11,6 +11,7 @@ log = { version = "0.4.14", default-features = false }
sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" }
sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" }
sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" }
frame-support = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" }
frame-system = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" }
@@ -20,7 +21,6 @@ xcm-executor = { path = "../xcm-executor", default-features = false }
[dev-dependencies]
pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" }
polkadot-runtime-parachains = { path = "../../runtime/parachains" }
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-io = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
xcm-builder = { path = "../xcm-builder" }
polkadot-parachain = { path = "../../parachain" }
@@ -31,6 +31,7 @@ std = [
"codec/std",
"serde",
"sp-std/std",
"sp-core/std",
"sp-runtime/std",
"frame-support/std",
"frame-system/std",
+58 -4
View File
@@ -50,8 +50,12 @@ pub mod pallet {
pallet_prelude::*,
};
use frame_system::{pallet_prelude::*, Config as SysConfig};
use sp_runtime::traits::{AccountIdConversion, BlockNumberProvider};
use xcm_executor::traits::{InvertLocation, OnResponse, WeightBounds};
use sp_core::H256;
use sp_runtime::traits::{AccountIdConversion, BlakeTwo256, BlockNumberProvider, Hash};
use xcm_executor::{
traits::{ClaimAssets, DropAssets, InvertLocation, OnResponse, WeightBounds},
Assets,
};
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
@@ -170,6 +174,10 @@ pub mod pallet {
///
/// \[ id \]
ResponseTaken(QueryId),
/// Some assets have been placed in an asset trap.
///
/// \[ hash, origin, assets \]
AssetsTrapped(H256, MultiLocation, VersionedMultiAssets),
}
#[pallet::origin]
@@ -236,6 +244,14 @@ pub mod pallet {
pub(super) type Queries<T: Config> =
StorageMap<_, Blake2_128Concat, QueryId, QueryStatus<T::BlockNumber>, OptionQuery>;
/// The existing asset traps.
///
/// Key is the blake2 256 hash of (origin, versioned `MultiAssets`) pair. Value is the number of
/// times this pair has been trapped (usually just 1 if it exists at all).
#[pallet::storage]
#[pallet::getter(fn asset_trap)]
pub(super) type AssetTraps<T: Config> = StorageMap<_, Identity, H256, u32, ValueQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
@@ -553,8 +569,47 @@ pub mod pallet {
}
}
impl<T: Config> DropAssets for Pallet<T> {
fn drop_assets(origin: &MultiLocation, assets: Assets) -> Weight {
if assets.is_empty() {
return 0
}
let versioned = VersionedMultiAssets::from(MultiAssets::from(assets));
let hash = BlakeTwo256::hash_of(&(&origin, &versioned));
AssetTraps::<T>::mutate(hash, |n| *n += 1);
Self::deposit_event(Event::AssetsTrapped(hash, origin.clone(), versioned));
// TODO: Put the real weight in there.
0
}
}
impl<T: Config> ClaimAssets for Pallet<T> {
fn claim_assets(
origin: &MultiLocation,
ticket: &MultiLocation,
assets: &MultiAssets,
) -> bool {
let mut versioned = VersionedMultiAssets::from(assets.clone());
match (ticket.parents, &ticket.interior) {
(0, X1(GeneralIndex(i))) =>
versioned = match versioned.into_version(*i as u32) {
Ok(v) => v,
Err(()) => return false,
},
(0, Here) => (),
_ => return false,
};
let hash = BlakeTwo256::hash_of(&(origin, versioned));
match AssetTraps::<T>::get(hash) {
0 => return false,
1 => AssetTraps::<T>::remove(hash),
n => AssetTraps::<T>::insert(hash, n - 1),
}
return true
}
}
impl<T: Config> OnResponse for Pallet<T> {
/// Returns `true` if we are expecting a response from `origin` for query `query_id`.
fn expecting_response(origin: &MultiLocation, query_id: QueryId) -> bool {
if let Some(QueryStatus::Pending { responder, .. }) = Queries::<T>::get(query_id) {
return MultiLocation::try_from(responder).map_or(false, |r| origin == &r)
@@ -562,7 +617,6 @@ pub mod pallet {
false
}
/// Handler for receiving a `response` from `origin` relating to `query_id`.
fn on_response(
origin: &MultiLocation,
query_id: QueryId,
+2
View File
@@ -251,6 +251,8 @@ impl xcm_executor::Config for XcmConfig {
type Weigher = FixedWeightBounds<BaseXcmWeight, Call, MaxInstructions>;
type Trader = FixedRateOfFungible<CurrencyPerSecond, ()>;
type ResponseHandler = XcmPallet;
type AssetTrap = XcmPallet;
type AssetClaims = XcmPallet;
}
pub type LocalOriginToLocation = SignedToAccountId32<Origin, AccountId, AnyNetwork>;
+81 -2
View File
@@ -14,11 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::{mock::*, QueryStatus};
use crate::{mock::*, AssetTraps, QueryStatus};
use frame_support::{assert_noop, assert_ok, traits::Currency};
use polkadot_parachain::primitives::{AccountIdConversion, Id as ParaId};
use sp_runtime::traits::{BlakeTwo256, Hash};
use std::convert::TryInto;
use xcm::{latest::prelude::*, VersionedXcm};
use xcm::{latest::prelude::*, VersionedMultiAssets, VersionedXcm};
use xcm_executor::XcmExecutor;
const ALICE: AccountId = AccountId::new([0u8; 32]);
@@ -295,3 +296,81 @@ fn execute_withdraw_to_deposit_works() {
);
});
}
/// Test drop/claim assets.
#[test]
fn trapped_assets_can_be_claimed() {
let balances = vec![(ALICE, INITIAL_BALANCE), (BOB, INITIAL_BALANCE)];
new_test_ext_with_balances(balances).execute_with(|| {
let weight = 6 * BaseXcmWeight::get();
let dest: MultiLocation =
Junction::AccountId32 { network: NetworkId::Any, id: BOB.into() }.into();
assert_ok!(XcmPallet::execute(
Origin::signed(ALICE),
Box::new(VersionedXcm::from(Xcm(vec![
WithdrawAsset((Here, SEND_AMOUNT).into()),
buy_execution((Here, SEND_AMOUNT)),
// Don't propagated the error into the result.
SetErrorHandler(Xcm(vec![ClearError])),
// This will make an error.
Trap(0),
// This would succeed, but we never get to it.
DepositAsset { assets: All.into(), max_assets: 1, beneficiary: dest.clone() },
]))),
weight
));
let source: MultiLocation =
Junction::AccountId32 { network: NetworkId::Any, id: ALICE.into() }.into();
let trapped = AssetTraps::<Test>::iter().collect::<Vec<_>>();
let vma = VersionedMultiAssets::from(MultiAssets::from((Here, SEND_AMOUNT)));
let hash = BlakeTwo256::hash_of(&(source.clone(), vma.clone()));
assert_eq!(
last_events(2),
vec![
Event::XcmPallet(crate::Event::AssetsTrapped(hash.clone(), source, vma)),
Event::XcmPallet(crate::Event::Attempted(Outcome::Complete(
5 * BaseXcmWeight::get()
)))
]
);
assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE - SEND_AMOUNT);
assert_eq!(Balances::total_balance(&BOB), INITIAL_BALANCE);
let expected = vec![(hash, 1u32)];
assert_eq!(trapped, expected);
let weight = 3 * BaseXcmWeight::get();
assert_ok!(XcmPallet::execute(
Origin::signed(ALICE),
Box::new(VersionedXcm::from(Xcm(vec![
ClaimAsset { assets: (Here, SEND_AMOUNT).into(), ticket: Here.into() },
buy_execution((Here, SEND_AMOUNT)),
DepositAsset { assets: All.into(), max_assets: 1, beneficiary: dest.clone() },
]))),
weight
));
assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE - SEND_AMOUNT);
assert_eq!(Balances::total_balance(&BOB), INITIAL_BALANCE + SEND_AMOUNT);
assert_eq!(AssetTraps::<Test>::iter().collect::<Vec<_>>(), vec![]);
let weight = 3 * BaseXcmWeight::get();
assert_ok!(XcmPallet::execute(
Origin::signed(ALICE),
Box::new(VersionedXcm::from(Xcm(vec![
ClaimAsset { assets: (Here, SEND_AMOUNT).into(), ticket: Here.into() },
buy_execution((Here, SEND_AMOUNT)),
DepositAsset { assets: All.into(), max_assets: 1, beneficiary: dest },
]))),
weight
));
assert_eq!(
last_event(),
Event::XcmPallet(crate::Event::Attempted(Outcome::Incomplete(
BaseXcmWeight::get(),
XcmError::UnknownClaim
)))
);
});
}
+10
View File
@@ -217,6 +217,16 @@ pub enum VersionedMultiAssets {
V1(v1::MultiAssets),
}
impl VersionedMultiAssets {
pub fn into_version(self, n: u32) -> Result<Self, ()> {
Ok(match n {
0 => Self::V0(self.try_into()?),
1 | 2 => Self::V1(self.try_into()?),
_ => return Err(()),
})
}
}
impl From<Vec<v0::MultiAsset>> for VersionedMultiAssets {
fn from(x: Vec<v0::MultiAsset>) -> Self {
VersionedMultiAssets::V0(x)
+58 -4
View File
@@ -392,6 +392,8 @@ pub enum Instruction<Call> {
/// prioritized under standard asset ordering. Any others will remain in holding.
/// - `beneficiary`: The new owner for the assets.
///
/// Kind: *Instruction*
///
/// Errors:
DepositAsset { assets: MultiAssetFilter, max_assets: u32, beneficiary: MultiLocation },
@@ -411,6 +413,8 @@ pub enum Instruction<Call> {
/// - `xcm`: The orders that should follow the `ReserveAssetDeposited` instruction
/// which is sent onwards to `dest`.
///
/// Kind: *Instruction*
///
/// Errors:
DepositReserveAsset {
assets: MultiAssetFilter,
@@ -428,6 +432,8 @@ pub enum Instruction<Call> {
/// - `give`: The asset(s) to remove from holding.
/// - `receive`: The minimum amount of assets(s) which `give` should be exchanged for.
///
/// Kind: *Instruction*
///
/// Errors:
ExchangeAsset { give: MultiAssetFilter, receive: MultiAssets },
@@ -442,6 +448,8 @@ pub enum Instruction<Call> {
/// - `xcm`: The instructions to execute on the assets once withdrawn *on the reserve
/// location*.
///
/// Kind: *Instruction*
///
/// Errors:
InitiateReserveWithdraw { assets: MultiAssetFilter, reserve: MultiLocation, xcm: Xcm<()> },
@@ -456,6 +464,8 @@ pub enum Instruction<Call> {
/// NOTE: The `dest` location *MUST* respect this origin as a valid teleportation origin for all
/// `assets`. If it does not, then the assets may be lost.
///
/// Kind: *Instruction*
///
/// Errors:
InitiateTeleport { assets: MultiAssetFilter, dest: MultiLocation, xcm: Xcm<()> },
@@ -472,6 +482,8 @@ pub enum Instruction<Call> {
/// is sent as a reply may take to execute. NOTE: If this is unexpectedly large then the
/// response may not execute at all.
///
/// Kind: *Instruction*
///
/// Errors:
QueryHolding {
#[codec(compact)]
@@ -490,13 +502,20 @@ pub enum Instruction<Call> {
/// expected maximum weight of the total XCM to be executed for the
/// `AllowTopLevelPaidExecutionFrom` barrier to allow the XCM be executed.
///
/// Kind: *Instruction*
///
/// Errors:
BuyExecution { fees: MultiAsset, weight_limit: WeightLimit },
/// Refund any surplus weight previously bought with `BuyExecution`.
///
/// Kind: *Instruction*
///
/// Errors: None.
RefundSurplus,
/// Set code that should be called in the case of an error happening.
/// Set the Error Handler Register. This is code that should be called in the case of an error
/// happening.
///
/// An error occurring within execution of this code will _NOT_ result in the error register
/// being set, nor will an error handler be called due to it. The error handler and appendix
@@ -505,10 +524,15 @@ pub enum Instruction<Call> {
/// The apparent weight of this instruction is inclusive of the inner `Xcm`; the executing
/// weight however includes only the difference between the previous handler and the new
/// handler, which can reasonably be negative, which would result in a surplus.
///
/// Kind: *Instruction*
///
/// Errors: None.
SetErrorHandler(Xcm<Call>),
/// Set code that should be called after code execution (including the error handler if any)
/// is finished. This will be called regardless of whether an error occurred.
/// Set the Appendix Register. This is code that should be called after code execution
/// (including the error handler if any) is finished. This will be called regardless of whether
/// an error occurred.
///
/// Any error occurring due to execution of this code will result in the error register being
/// set, and the error handler (if set) firing.
@@ -516,10 +540,38 @@ pub enum Instruction<Call> {
/// The apparent weight of this instruction is inclusive of the inner `Xcm`; the executing
/// weight however includes only the difference between the previous appendix and the new
/// appendix, which can reasonably be negative, which would result in a surplus.
///
/// Kind: *Instruction*
///
/// Errors: None.
SetAppendix(Xcm<Call>),
/// Clear the error register.
/// Clear the Error Register.
///
/// Kind: *Instruction*
///
/// Errors: None.
ClearError,
/// Create some assets which are being held on behalf of the origin.
///
/// - `assets`: The assets which are to be claimed. This must match exactly with the assets
/// claimable by the origin of the ticket.
/// - `ticket`: The ticket of the asset; this is an abstract identifier to help locate the
/// asset.
///
/// Kind: *Instruction*
///
/// Errors:
ClaimAsset { assets: MultiAssets, ticket: MultiLocation },
/// Always throws an error of type `Trap`.
///
/// Kind: *Instruction*
///
/// Errors:
/// - `Trap`: All circumstances, whose inner value is the same as this item's inner value.
Trap(u64),
}
impl<Call> Xcm<Call> {
@@ -572,6 +624,8 @@ impl<Call> Instruction<Call> {
SetErrorHandler(xcm) => SetErrorHandler(xcm.into()),
SetAppendix(xcm) => SetAppendix(xcm.into()),
ClearError => ClearError,
ClaimAsset { assets, ticket } => ClaimAsset { assets, ticket },
Trap(code) => Trap(code),
}
}
}
+4
View File
@@ -94,6 +94,10 @@ pub enum Error {
Unroutable,
/// The weight required was not specified when it should have been.
UnknownWeightRequired,
/// An error was intentionally forced. A code is included.
Trap(u64),
/// The given claim could not be recognized/found.
UnknownClaim,
}
impl From<()> for Error {
+8 -8
View File
@@ -30,7 +30,7 @@ use xcm_executor::traits::{OnResponse, ShouldExecute};
pub struct TakeWeightCredit;
impl ShouldExecute for TakeWeightCredit {
fn should_execute<Call>(
_origin: &Option<MultiLocation>,
_origin: &MultiLocation,
_top_level: bool,
_message: &mut Xcm<Call>,
max_weight: Weight,
@@ -49,19 +49,21 @@ impl ShouldExecute for TakeWeightCredit {
pub struct AllowTopLevelPaidExecutionFrom<T>(PhantomData<T>);
impl<T: Contains<MultiLocation>> ShouldExecute for AllowTopLevelPaidExecutionFrom<T> {
fn should_execute<Call>(
origin: &Option<MultiLocation>,
origin: &MultiLocation,
top_level: bool,
message: &mut Xcm<Call>,
max_weight: Weight,
_weight_credit: &mut Weight,
) -> Result<(), ()> {
let origin = origin.as_ref().ok_or(())?;
ensure!(T::contains(origin), ());
ensure!(top_level, ());
let mut iter = message.0.iter_mut();
let i = iter.next().ok_or(())?;
match i {
ReceiveTeleportedAsset(..) | WithdrawAsset(..) | ReserveAssetDeposited(..) => (),
ReceiveTeleportedAsset(..) |
WithdrawAsset(..) |
ReserveAssetDeposited(..) |
ClaimAsset { .. } => (),
_ => return Err(()),
}
let mut i = iter.next().ok_or(())?;
@@ -87,13 +89,12 @@ impl<T: Contains<MultiLocation>> ShouldExecute for AllowTopLevelPaidExecutionFro
pub struct AllowUnpaidExecutionFrom<T>(PhantomData<T>);
impl<T: Contains<MultiLocation>> ShouldExecute for AllowUnpaidExecutionFrom<T> {
fn should_execute<Call>(
origin: &Option<MultiLocation>,
origin: &MultiLocation,
_top_level: bool,
_message: &mut Xcm<Call>,
_max_weight: Weight,
_weight_credit: &mut Weight,
) -> Result<(), ()> {
let origin = origin.as_ref().ok_or(())?;
ensure!(T::contains(origin), ());
Ok(())
}
@@ -115,13 +116,12 @@ impl<ParaId: IsSystem + From<u32>> Contains<MultiLocation> for IsChildSystemPara
pub struct AllowKnownQueryResponses<ResponseHandler>(PhantomData<ResponseHandler>);
impl<ResponseHandler: OnResponse> ShouldExecute for AllowKnownQueryResponses<ResponseHandler> {
fn should_execute<Call>(
origin: &Option<MultiLocation>,
origin: &MultiLocation,
_top_level: bool,
message: &mut Xcm<Call>,
_max_weight: Weight,
_weight_credit: &mut Weight,
) -> Result<(), ()> {
let origin = origin.as_ref().ok_or(())?;
match message.0.first() {
Some(QueryResponse { query_id, .. })
if ResponseHandler::expecting_response(origin, *query_id) =>
+34
View File
@@ -35,6 +35,7 @@ pub use sp_std::{
marker::PhantomData,
};
pub use xcm::latest::prelude::*;
use xcm_executor::traits::{ClaimAssets, DropAssets};
pub use xcm_executor::{
traits::{ConvertOrigin, FilterAssetLocation, InvertLocation, OnResponse, TransactAsset},
Assets, Config,
@@ -269,6 +270,37 @@ pub type TestBarrier = (
AllowUnpaidExecutionFrom<IsInVec<AllowUnpaidFrom>>,
);
parameter_types! {
pub static TrappedAssets: Vec<(MultiLocation, MultiAssets)> = vec![];
}
pub struct TestAssetTrap;
impl DropAssets for TestAssetTrap {
fn drop_assets(origin: &MultiLocation, assets: Assets) -> Weight {
let mut t: Vec<(MultiLocation, MultiAssets)> = TrappedAssets::get();
t.push((origin.clone(), assets.into()));
TrappedAssets::set(t);
5
}
}
impl ClaimAssets for TestAssetTrap {
fn claim_assets(origin: &MultiLocation, ticket: &MultiLocation, what: &MultiAssets) -> bool {
let mut t: Vec<(MultiLocation, MultiAssets)> = TrappedAssets::get();
if let (0, X1(GeneralIndex(i))) = (ticket.parents, &ticket.interior) {
if let Some((l, a)) = t.get(*i as usize) {
if l == origin && a == what {
t.swap_remove(*i as usize);
TrappedAssets::set(t);
return true
}
}
}
false
}
}
pub struct TestConfig;
impl Config for TestConfig {
type Call = TestCall;
@@ -282,4 +314,6 @@ impl Config for TestConfig {
type Weigher = FixedWeightBounds<UnitWeightCost, TestCall, MaxInstructions>;
type Trader = FixedRateOfFungible<WeightPrice, ()>;
type ResponseHandler = TestResponseHandler;
type AssetTrap = TestAssetTrap;
type AssetClaims = TestAssetTrap;
}
+121 -8
View File
@@ -58,7 +58,7 @@ fn take_weight_credit_barrier_should_work() {
Xcm::<()>(vec![TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }]);
let mut weight_credit = 10;
let r = TakeWeightCredit::should_execute(
&Some(Parent.into()),
&Parent.into(),
true,
&mut message,
10,
@@ -68,7 +68,7 @@ fn take_weight_credit_barrier_should_work() {
assert_eq!(weight_credit, 0);
let r = TakeWeightCredit::should_execute(
&Some(Parent.into()),
&Parent.into(),
true,
&mut message,
10,
@@ -86,7 +86,7 @@ fn allow_unpaid_should_work() {
AllowUnpaidFrom::set(vec![Parent.into()]);
let r = AllowUnpaidExecutionFrom::<IsInVec<AllowUnpaidFrom>>::should_execute(
&Some(Parachain(1).into()),
&Parachain(1).into(),
true,
&mut message,
10,
@@ -95,7 +95,7 @@ fn allow_unpaid_should_work() {
assert_eq!(r, Err(()));
let r = AllowUnpaidExecutionFrom::<IsInVec<AllowUnpaidFrom>>::should_execute(
&Some(Parent.into()),
&Parent.into(),
true,
&mut message,
10,
@@ -112,7 +112,7 @@ fn allow_paid_should_work() {
Xcm::<()>(vec![TransferAsset { assets: (Parent, 100).into(), beneficiary: Here.into() }]);
let r = AllowTopLevelPaidExecutionFrom::<IsInVec<AllowPaidFrom>>::should_execute(
&Some(Parachain(1).into()),
&Parachain(1).into(),
true,
&mut message,
10,
@@ -128,7 +128,7 @@ fn allow_paid_should_work() {
]);
let r = AllowTopLevelPaidExecutionFrom::<IsInVec<AllowPaidFrom>>::should_execute(
&Some(Parent.into()),
&Parent.into(),
true,
&mut underpaying_message,
30,
@@ -144,7 +144,7 @@ fn allow_paid_should_work() {
]);
let r = AllowTopLevelPaidExecutionFrom::<IsInVec<AllowPaidFrom>>::should_execute(
&Some(Parachain(1).into()),
&Parachain(1).into(),
true,
&mut paying_message,
30,
@@ -153,7 +153,7 @@ fn allow_paid_should_work() {
assert_eq!(r, Err(()));
let r = AllowTopLevelPaidExecutionFrom::<IsInVec<AllowPaidFrom>>::should_execute(
&Some(Parent.into()),
&Parent.into(),
true,
&mut paying_message,
30,
@@ -202,6 +202,119 @@ fn transfer_should_work() {
assert_eq!(sent_xcm(), vec![]);
}
#[test]
fn basic_asset_trap_should_work() {
// we'll let them have message execution for free.
AllowUnpaidFrom::set(vec![X1(Parachain(1)).into(), X1(Parachain(2)).into()]);
// Child parachain #1 owns 1000 tokens held by us in reserve.
add_asset(1001, (Here, 1000));
// They want to transfer 100 of them to their sibling parachain #2 but have a problem
let r = XcmExecutor::<TestConfig>::execute_xcm(
Parachain(1).into(),
Xcm(vec![
WithdrawAsset((Here, 100).into()),
DepositAsset {
assets: Wild(All),
max_assets: 0, //< Whoops!
beneficiary: AccountIndex64 { index: 3, network: Any }.into(),
},
]),
20,
);
assert_eq!(r, Outcome::Complete(25));
assert_eq!(assets(1001), vec![(Here, 900).into()]);
assert_eq!(assets(3), vec![]);
// Incorrect ticket doesn't work.
let old_trapped_assets = TrappedAssets::get();
let r = XcmExecutor::<TestConfig>::execute_xcm(
Parachain(1).into(),
Xcm(vec![
ClaimAsset { assets: (Here, 100).into(), ticket: GeneralIndex(1).into() },
DepositAsset {
assets: Wild(All),
max_assets: 1,
beneficiary: AccountIndex64 { index: 3, network: Any }.into(),
},
]),
20,
);
assert_eq!(r, Outcome::Incomplete(10, XcmError::UnknownClaim));
assert_eq!(assets(1001), vec![(Here, 900).into()]);
assert_eq!(assets(3), vec![]);
assert_eq!(old_trapped_assets, TrappedAssets::get());
// Incorrect origin doesn't work.
let old_trapped_assets = TrappedAssets::get();
let r = XcmExecutor::<TestConfig>::execute_xcm(
Parachain(2).into(),
Xcm(vec![
ClaimAsset { assets: (Here, 100).into(), ticket: GeneralIndex(0).into() },
DepositAsset {
assets: Wild(All),
max_assets: 1,
beneficiary: AccountIndex64 { index: 3, network: Any }.into(),
},
]),
20,
);
assert_eq!(r, Outcome::Incomplete(10, XcmError::UnknownClaim));
assert_eq!(assets(1001), vec![(Here, 900).into()]);
assert_eq!(assets(3), vec![]);
assert_eq!(old_trapped_assets, TrappedAssets::get());
// Incorrect assets doesn't work.
let old_trapped_assets = TrappedAssets::get();
let r = XcmExecutor::<TestConfig>::execute_xcm(
Parachain(1).into(),
Xcm(vec![
ClaimAsset { assets: (Here, 101).into(), ticket: GeneralIndex(0).into() },
DepositAsset {
assets: Wild(All),
max_assets: 1,
beneficiary: AccountIndex64 { index: 3, network: Any }.into(),
},
]),
20,
);
assert_eq!(r, Outcome::Incomplete(10, XcmError::UnknownClaim));
assert_eq!(assets(1001), vec![(Here, 900).into()]);
assert_eq!(assets(3), vec![]);
assert_eq!(old_trapped_assets, TrappedAssets::get());
let r = XcmExecutor::<TestConfig>::execute_xcm(
Parachain(1).into(),
Xcm(vec![
ClaimAsset { assets: (Here, 100).into(), ticket: GeneralIndex(0).into() },
DepositAsset {
assets: Wild(All),
max_assets: 1,
beneficiary: AccountIndex64 { index: 3, network: Any }.into(),
},
]),
20,
);
assert_eq!(r, Outcome::Complete(20));
assert_eq!(assets(1001), vec![(Here, 900).into()]);
assert_eq!(assets(3), vec![(Here, 100).into()]);
// Same again doesn't work :-)
let r = XcmExecutor::<TestConfig>::execute_xcm(
Parachain(1).into(),
Xcm(vec![
ClaimAsset { assets: (Here, 100).into(), ticket: GeneralIndex(0).into() },
DepositAsset {
assets: Wild(All),
max_assets: 1,
beneficiary: AccountIndex64 { index: 3, network: Any }.into(),
},
]),
20,
);
assert_eq!(r, Outcome::Incomplete(10, XcmError::UnknownClaim));
}
#[test]
fn errors_should_return_unused_weight() {
// we'll let them have message execution for free.
+3 -1
View File
@@ -166,7 +166,9 @@ impl xcm_executor::Config for XcmConfig {
type Barrier = Barrier;
type Weigher = FixedWeightBounds<BaseXcmWeight, Call, MaxInstructions>;
type Trader = FixedRateOfFungible<KsmPerSecond, ()>;
type ResponseHandler = ();
type ResponseHandler = XcmPallet;
type AssetTrap = XcmPallet;
type AssetClaims = XcmPallet;
}
pub type LocalOriginToLocation = SignedToAccountId32<Origin, AccountId, KusamaNetwork>;
+5
View File
@@ -94,6 +94,11 @@ impl Assets {
self.fungible.len() + self.non_fungible.len()
}
/// Returns `true` if `self` contains no assets.
pub fn is_empty(&self) -> bool {
self.fungible.is_empty() && self.non_fungible.is_empty()
}
/// A borrowing iterator over the fungible assets.
pub fn fungible_assets_iter<'a>(&'a self) -> impl Iterator<Item = MultiAsset> + 'a {
self.fungible
+9 -2
View File
@@ -15,8 +15,8 @@
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::traits::{
ConvertOrigin, FilterAssetLocation, InvertLocation, OnResponse, ShouldExecute, TransactAsset,
WeightBounds, WeightTrader,
ClaimAssets, ConvertOrigin, DropAssets, FilterAssetLocation, InvertLocation, OnResponse,
ShouldExecute, TransactAsset, WeightBounds, WeightTrader,
};
use frame_support::{
dispatch::{Dispatchable, Parameter},
@@ -58,4 +58,11 @@ pub trait Config {
/// What to do when a response of a query is found.
type ResponseHandler: OnResponse;
/// The general asset trap - handler for when assets are left in the Holding Register at the
/// end of execution.
type AssetTrap: DropAssets;
/// The handler for when there is an instruction to claim assets.
type AssetClaims: ClaimAssets;
}
+25 -13
View File
@@ -31,8 +31,8 @@ use xcm::latest::{
pub mod traits;
use traits::{
ConvertOrigin, FilterAssetLocation, InvertLocation, OnResponse, ShouldExecute, TransactAsset,
WeightBounds, WeightTrader,
ClaimAssets, ConvertOrigin, DropAssets, FilterAssetLocation, InvertLocation, OnResponse,
ShouldExecute, TransactAsset, WeightBounds, WeightTrader,
};
mod assets;
@@ -86,7 +86,6 @@ impl<Config: config::Config> ExecuteXcm<Config::Call> for XcmExecutor<Config> {
if xcm_weight > weight_limit {
return Outcome::Error(XcmError::WeightLimitReached(xcm_weight))
}
let origin = Some(origin);
if let Err(_) = Config::Barrier::should_execute(
&origin,
@@ -98,7 +97,7 @@ impl<Config: config::Config> ExecuteXcm<Config::Call> for XcmExecutor<Config> {
return Outcome::Error(XcmError::Barrier)
}
let mut vm = Self::new(origin);
let mut vm = Self::new(origin.clone());
while !message.0.is_empty() {
let result = vm.execute(message);
@@ -116,9 +115,12 @@ impl<Config: config::Config> ExecuteXcm<Config::Call> for XcmExecutor<Config> {
vm.refund_surplus();
drop(vm.trader);
// TODO #2841: Do something with holding? (Fail-safe AssetTrap?)
let mut weight_used = xcm_weight.saturating_sub(vm.total_surplus);
if !vm.holding.is_empty() {
weight_used.saturating_accrue(Config::AssetTrap::drop_assets(&origin, vm.holding));
};
let weight_used = xcm_weight.saturating_sub(vm.total_surplus);
match vm.error {
None => Outcome::Complete(weight_used),
// TODO: #2841 #REALWEIGHT We should deduct the cost of any instructions following
@@ -129,10 +131,10 @@ impl<Config: config::Config> ExecuteXcm<Config::Call> for XcmExecutor<Config> {
}
impl<Config: config::Config> XcmExecutor<Config> {
fn new(origin: Option<MultiLocation>) -> Self {
fn new(origin: MultiLocation) -> Self {
Self {
holding: Assets::new(),
origin,
origin: Some(origin),
trader: Config::Trader::new(),
error: None,
total_surplus: 0,
@@ -183,7 +185,7 @@ impl<Config: config::Config> XcmExecutor<Config> {
/// Drop the registered error handler and refund its weight.
fn drop_error_handler(&mut self) {
self.error_handler = Xcm::<Config::Call>(vec![]);
self.total_surplus = self.total_surplus.saturating_add(self.error_handler_weight);
self.total_surplus.saturating_accrue(self.error_handler_weight);
self.error_handler_weight = 0;
}
@@ -199,7 +201,7 @@ impl<Config: config::Config> XcmExecutor<Config> {
fn refund_surplus(&mut self) {
let current_surplus = self.total_surplus.saturating_sub(self.total_refunded);
if current_surplus > 0 {
self.total_refunded = self.total_refunded.saturating_add(current_surplus);
self.total_refunded.saturating_accrue(current_surplus);
if let Some(w) = self.trader.refund_weight(current_surplus) {
self.holding.subsume(w);
}
@@ -300,7 +302,7 @@ impl<Config: config::Config> XcmExecutor<Config> {
// reported back to the caller and this ensures that they account for the total
// weight consumed correctly (potentially allowing them to do more operations in a
// block than they otherwise would).
self.total_surplus = self.total_surplus.saturating_add(surplus);
self.total_surplus.saturating_accrue(surplus);
Ok(())
},
QueryResponse { query_id, response, max_weight } => {
@@ -390,14 +392,14 @@ impl<Config: config::Config> XcmExecutor<Config> {
},
SetErrorHandler(mut handler) => {
let handler_weight = Config::Weigher::weight(&mut handler)?;
self.total_surplus = self.total_surplus.saturating_add(self.error_handler_weight);
self.total_surplus.saturating_accrue(self.error_handler_weight);
self.error_handler = handler;
self.error_handler_weight = handler_weight;
Ok(())
},
SetAppendix(mut appendix) => {
let appendix_weight = Config::Weigher::weight(&mut appendix)?;
self.total_surplus = self.total_surplus.saturating_add(self.appendix_weight);
self.total_surplus.saturating_accrue(self.appendix_weight);
self.appendix = appendix;
self.appendix_weight = appendix_weight;
Ok(())
@@ -406,6 +408,16 @@ impl<Config: config::Config> XcmExecutor<Config> {
self.error = None;
Ok(())
},
ClaimAsset { assets, ticket } => {
let origin = self.origin.as_ref().ok_or(XcmError::BadOrigin)?;
let ok = Config::AssetClaims::claim_assets(origin, &ticket, &assets);
ensure!(ok, XcmError::UnknownClaim);
for asset in assets.drain().into_iter() {
self.holding.subsume(asset);
}
Ok(())
},
Trap(code) => Err(XcmError::Trap(code)),
ExchangeAsset { .. } => Err(XcmError::Unimplemented),
HrmpNewChannelOpenRequest { .. } => Err(XcmError::Unimplemented),
HrmpChannelAccepted { .. } => Err(XcmError::Unimplemented),
@@ -0,0 +1,79 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::Assets;
use core::marker::PhantomData;
use frame_support::{traits::Contains, weights::Weight};
use xcm::latest::{MultiAssets, MultiLocation};
/// Define a handler for when some non-empty `Assets` value should be dropped.
pub trait DropAssets {
/// Handler for receiving dropped assets. Returns the weight consumed by this operation.
fn drop_assets(origin: &MultiLocation, assets: Assets) -> Weight;
}
impl DropAssets for () {
fn drop_assets(_origin: &MultiLocation, _assets: Assets) -> Weight {
0
}
}
/// Morph a given `DropAssets` implementation into one which can filter based on assets. This can
/// be used to ensure that `Assets` values which hold no value are ignored.
pub struct FilterAssets<D, A>(PhantomData<(D, A)>);
impl<D: DropAssets, A: Contains<Assets>> DropAssets for FilterAssets<D, A> {
fn drop_assets(origin: &MultiLocation, assets: Assets) -> Weight {
if A::contains(&assets) {
D::drop_assets(origin, assets)
} else {
0
}
}
}
/// Morph a given `DropAssets` implementation into one which can filter based on origin. This can
/// be used to ban origins which don't have proper protections/policies against misuse of the
/// asset trap facility don't get to use it.
pub struct FilterOrigin<D, O>(PhantomData<(D, O)>);
impl<D: DropAssets, O: Contains<MultiLocation>> DropAssets for FilterOrigin<D, O> {
fn drop_assets(origin: &MultiLocation, assets: Assets) -> Weight {
if O::contains(origin) {
D::drop_assets(origin, assets)
} else {
0
}
}
}
/// Define any handlers for the `AssetClaim` instruction.
pub trait ClaimAssets {
/// Claim any assets available to `origin` and return them in a single `Assets` value, together
/// with the weight used by this operation.
fn claim_assets(origin: &MultiLocation, ticket: &MultiLocation, what: &MultiAssets) -> bool;
}
#[impl_trait_for_tuples::impl_for_tuples(30)]
impl ClaimAssets for Tuple {
fn claim_assets(origin: &MultiLocation, ticket: &MultiLocation, what: &MultiAssets) -> bool {
for_tuples!( #(
if Tuple::claim_assets(origin, ticket, what) {
return true;
}
)* );
false
}
}
@@ -18,6 +18,8 @@
mod conversion;
pub use conversion::{Convert, ConvertOrigin, Decoded, Encoded, Identity, InvertLocation, JustTry};
mod drop_assets;
pub use drop_assets::{ClaimAssets, DropAssets};
mod filter_asset_location;
pub use filter_asset_location::FilterAssetLocation;
mod matches_fungible;
@@ -34,7 +34,7 @@ pub trait ShouldExecute {
/// message may utilize in its execution. Typically non-zero only because of prior fee
/// payment, but could in principle be due to other factors.
fn should_execute<Call>(
origin: &Option<MultiLocation>,
origin: &MultiLocation,
top_level: bool,
message: &mut Xcm<Call>,
max_weight: Weight,
@@ -45,7 +45,7 @@ pub trait ShouldExecute {
#[impl_trait_for_tuples::impl_for_tuples(30)]
impl ShouldExecute for Tuple {
fn should_execute<Call>(
origin: &Option<MultiLocation>,
origin: &MultiLocation,
top_level: bool,
message: &mut Xcm<Call>,
max_weight: Weight,
@@ -143,6 +143,8 @@ impl Config for XcmConfig {
type Weigher = FixedWeightBounds<UnitWeightCost, Call, MaxInstructions>;
type Trader = FixedRateOfFungible<KsmPerSecond, ()>;
type ResponseHandler = ();
type AssetTrap = ();
type AssetClaims = ();
}
#[frame_support::pallet]
@@ -133,6 +133,8 @@ impl Config for XcmConfig {
type Weigher = FixedWeightBounds<BaseXcmWeight, Call, MaxInstructions>;
type Trader = FixedRateOfFungible<KsmPerSecond, ()>;
type ResponseHandler = ();
type AssetTrap = ();
type AssetClaims = ();
}
pub type LocalOriginToLocation = SignedToAccountId32<Origin, AccountId, KusamaNetwork>;