Add claim_assets extrinsic to pallet-xcm (#3403)

If an XCM execution fails or ends with leftover assets, these will be
trapped.
In order to claim them, a custom XCM has to be executed, with the
`ClaimAsset` instruction.
However, arbitrary XCM execution is not allowed everywhere yet and XCM
itself is still not easy enough to use for users out there with trapped
assets.
This new extrinsic in `pallet-xcm` will allow these users to easily
claim their assets, without concerning themselves with writing arbitrary
XCMs.

Part of fixing https://github.com/paritytech/polkadot-sdk/issues/3495

---------

Co-authored-by: command-bot <>
Co-authored-by: Adrian Catangiu <adrian@parity.io>
This commit is contained in:
Francisco Aguirre
2024-03-01 08:31:48 +01:00
committed by GitHub
parent c0e52a9ed6
commit 650886683d
29 changed files with 1521 additions and 1063 deletions
+20 -1
View File
@@ -79,6 +79,13 @@ pub trait Config: crate::Config {
fn set_up_complex_asset_transfer() -> Option<(Assets, u32, Location, Box<dyn FnOnce()>)> {
None
}
/// Gets an asset that can be handled by the AssetTransactor.
///
/// Used only in benchmarks.
///
/// Used, for example, in the benchmark for `claim_assets`.
fn get_asset() -> Asset;
}
benchmarks! {
@@ -341,11 +348,23 @@ benchmarks! {
u32::MAX,
).unwrap()).collect::<Vec<_>>();
crate::Pallet::<T>::expect_response(query_id, Response::PalletsInfo(infos.try_into().unwrap()));
}: {
<crate::Pallet::<T> as QueryHandler>::take_response(query_id);
}
claim_assets {
let claim_origin = RawOrigin::Signed(whitelisted_caller());
let claim_location = T::ExecuteXcmOrigin::try_origin(claim_origin.clone().into()).map_err(|_| BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))?;
let asset: Asset = T::get_asset();
// Trap assets for claiming later
crate::Pallet::<T>::drop_assets(
&claim_location,
asset.clone().into(),
&XcmContext { origin: None, message_id: [0u8; 32], topic: None }
);
let versioned_assets = VersionedAssets::V4(asset.into());
}: _<RuntimeOrigin<T>>(claim_origin.into(), Box::new(versioned_assets), Box::new(VersionedLocation::V4(claim_location)))
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext_with_balances(Vec::new()),
+63
View File
@@ -85,6 +85,7 @@ pub trait WeightInfo {
fn migrate_and_notify_old_targets() -> Weight;
fn new_query() -> Weight;
fn take_response() -> Weight;
fn claim_assets() -> Weight;
}
/// fallback implementation
@@ -165,6 +166,10 @@ impl WeightInfo for TestWeightInfo {
fn take_response() -> Weight {
Weight::from_parts(100_000_000, 0)
}
fn claim_assets() -> Weight {
Weight::from_parts(100_000_000, 0)
}
}
#[frame_support::pallet]
@@ -1386,6 +1391,64 @@ pub mod pallet {
weight_limit,
)
}
/// Claims assets trapped on this pallet because of leftover assets during XCM execution.
///
/// - `origin`: Anyone can call this extrinsic.
/// - `assets`: The exact assets that were trapped. Use the version to specify what version
/// was the latest when they were trapped.
/// - `beneficiary`: The location/account where the claimed assets will be deposited.
#[pallet::call_index(12)]
#[pallet::weight({
let assets_version = assets.identify_version();
let maybe_assets: Result<Assets, ()> = (*assets.clone()).try_into();
let maybe_beneficiary: Result<Location, ()> = (*beneficiary.clone()).try_into();
match (maybe_assets, maybe_beneficiary) {
(Ok(assets), Ok(beneficiary)) => {
let ticket: Location = GeneralIndex(assets_version as u128).into();
let mut message = Xcm(vec![
ClaimAsset { assets: assets.clone(), ticket },
DepositAsset { assets: AllCounted(assets.len() as u32).into(), beneficiary },
]);
T::Weigher::weight(&mut message).map_or(Weight::MAX, |w| T::WeightInfo::claim_assets().saturating_add(w))
}
_ => Weight::MAX
}
})]
pub fn claim_assets(
origin: OriginFor<T>,
assets: Box<VersionedAssets>,
beneficiary: Box<VersionedLocation>,
) -> DispatchResult {
let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?;
log::debug!(target: "xcm::pallet_xcm::claim_assets", "origin: {:?}, assets: {:?}, beneficiary: {:?}", origin_location, assets, beneficiary);
// Extract version from `assets`.
let assets_version = assets.identify_version();
let assets: Assets = (*assets).try_into().map_err(|()| Error::<T>::BadVersion)?;
let number_of_assets = assets.len() as u32;
let beneficiary: Location =
(*beneficiary).try_into().map_err(|()| Error::<T>::BadVersion)?;
let ticket: Location = GeneralIndex(assets_version as u128).into();
let mut message = Xcm(vec![
ClaimAsset { assets, ticket },
DepositAsset { assets: AllCounted(number_of_assets).into(), beneficiary },
]);
let weight =
T::Weigher::weight(&mut message).map_err(|()| Error::<T>::UnweighableMessage)?;
let mut hash = message.using_encoded(sp_io::hashing::blake2_256);
let outcome = T::XcmExecutor::prepare_and_execute(
origin_location,
message,
&mut hash,
weight,
weight,
);
outcome.ensure_complete().map_err(|error| {
log::error!(target: "xcm::pallet_xcm::claim_assets", "XCM execution failed with error: {:?}", error);
Error::<T>::LocalExecutionIncomplete
})?;
Ok(())
}
}
}
+5 -1
View File
@@ -172,7 +172,7 @@ impl SendXcm for TestSendXcm {
msg: &mut Option<Xcm<()>>,
) -> SendResult<(Location, Xcm<()>)> {
if FAIL_SEND_XCM.with(|q| *q.borrow()) {
return Err(SendError::Transport("Intentional send failure used in tests"))
return Err(SendError::Transport("Intentional send failure used in tests"));
}
let pair = (dest.take().unwrap(), msg.take().unwrap());
Ok((pair, Assets::new()))
@@ -654,6 +654,10 @@ impl super::benchmarking::Config for Test {
});
Some((assets, fee_index as u32, dest, verify))
}
fn get_asset() -> Asset {
Asset { id: AssetId(Location::here()), fun: Fungible(ExistentialDeposit::get()) }
}
}
pub(crate) fn last_event() -> RuntimeEvent {
+51
View File
@@ -467,6 +467,57 @@ fn trapped_assets_can_be_claimed() {
});
}
// Like `trapped_assets_can_be_claimed` but using the `claim_assets` extrinsic.
#[test]
fn claim_assets_works() {
let balances = vec![(ALICE, INITIAL_BALANCE)];
new_test_ext_with_balances(balances).execute_with(|| {
// First trap some assets.
let trapping_program =
Xcm::builder_unsafe().withdraw_asset((Here, SEND_AMOUNT).into()).build();
// Even though assets are trapped, the extrinsic returns success.
assert_ok!(XcmPallet::execute(
RuntimeOrigin::signed(ALICE),
Box::new(VersionedXcm::V4(trapping_program)),
BaseXcmWeight::get() * 2,
));
assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE - SEND_AMOUNT);
// Expected `AssetsTrapped` event info.
let source: Location = Junction::AccountId32 { network: None, id: ALICE.into() }.into();
let versioned_assets = VersionedAssets::V4(Assets::from((Here, SEND_AMOUNT)));
let hash = BlakeTwo256::hash_of(&(source.clone(), versioned_assets.clone()));
// Assets were indeed trapped.
assert_eq!(
last_events(2),
vec![
RuntimeEvent::XcmPallet(crate::Event::AssetsTrapped {
hash,
origin: source,
assets: versioned_assets
}),
RuntimeEvent::XcmPallet(crate::Event::Attempted {
outcome: Outcome::Complete { used: BaseXcmWeight::get() * 1 }
})
],
);
let trapped = AssetTraps::<Test>::iter().collect::<Vec<_>>();
assert_eq!(trapped, vec![(hash, 1)]);
// Now claim them with the extrinsic.
assert_ok!(XcmPallet::claim_assets(
RuntimeOrigin::signed(ALICE),
Box::new(VersionedAssets::V4((Here, SEND_AMOUNT).into())),
Box::new(VersionedLocation::V4(
AccountId32 { network: None, id: ALICE.clone().into() }.into()
)),
));
assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE);
assert_eq!(AssetTraps::<Test>::iter().collect::<Vec<_>>(), vec![]);
});
}
/// Test failure to complete execution reverts intermediate side-effects.
///
/// XCM program will withdraw and deposit some assets, then fail execution of a further withdraw.