Skip to content

Commit

Permalink
pallet-xcm: add reserve_withdraw_assets() support
Browse files Browse the repository at this point in the history
pallet-xcm already offers `reserve_transfer_assets()` call for easy
reserve-based asset transfer, where original assets are reserved in
some reserve location and equivalent assets are minted at destination.

This commit adds `reserve_withdraw_assets()` call for users to be able
to do the operation reverse too: burn equivalent assets and release
original ones from reserve location.

Co-authored-by: Aleksandr Krupenkin <mail@akru.me>
  • Loading branch information
acatangiu and akru committed Sep 6, 2023
1 parent 6040147 commit 0b21a04
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
.saturating_add(Weight::from_parts(0, 1489))
.saturating_add(T::DbWeight::get().reads(1))
}
fn reserve_withdraw_assets() -> Weight {
Weight::from_parts(20_000_000, 0)
}
/// Storage: `Benchmark::Override` (r:0 w:0)
/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
fn execute() -> Weight {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
.saturating_add(Weight::from_parts(0, 1489))
.saturating_add(T::DbWeight::get().reads(1))
}
fn reserve_withdraw_assets() -> Weight {
Weight::from_parts(20_000_000, 0)
}
/// Storage: `Benchmark::Override` (r:0 w:0)
/// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`)
fn execute() -> Weight {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ impl<T: frame_system::Config> pallet_xcm::WeightInfo for WeightInfo<T> {
.saturating_add(Weight::from_parts(0, 1489))
.saturating_add(T::DbWeight::get().reads(1))
}
fn reserve_withdraw_assets() -> Weight {
Weight::from_parts(20_000_000, 0)
}
fn execute() -> Weight {
// Proof Size summary in bytes:
// Measured: `0`
Expand Down
18 changes: 18 additions & 0 deletions polkadot/xcm/pallet-xcm/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ benchmarks! {
let versioned_assets: VersionedMultiAssets = asset.into();
}: _<RuntimeOrigin<T>>(send_origin, Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0)

reserve_withdraw_assets {
let send_origin =
T::ExecuteXcmOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
let origin_location = T::ExecuteXcmOrigin::try_origin(send_origin.clone())
.map_err(|_| BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)))?;

let recipient = [0u8; 32];
let dest = T::ReachableDest::get().ok_or(
BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)),
)?;
// use native asset of destination
let asset: MultiAsset = (dest.clone(), 10).into();
let versioned_dest: VersionedMultiLocation = dest.into();
let versioned_beneficiary: VersionedMultiLocation =
AccountId32 { network: None, id: recipient.into() }.into();
let versioned_assets: VersionedMultiAssets = asset.into();
}: _<RuntimeOrigin<T>>(send_origin, Box::new(versioned_dest), Box::new(versioned_beneficiary), Box::new(versioned_assets), 0)

execute {
let execute_origin =
T::ExecuteXcmOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
Expand Down
175 changes: 175 additions & 0 deletions polkadot/xcm/pallet-xcm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub trait WeightInfo {
fn send() -> Weight;
fn teleport_assets() -> Weight;
fn reserve_transfer_assets() -> Weight;
fn reserve_withdraw_assets() -> Weight;
fn execute() -> Weight;
fn force_xcm_version() -> Weight;
fn force_default_xcm_version() -> Weight;
Expand Down Expand Up @@ -90,6 +91,10 @@ impl WeightInfo for TestWeightInfo {
Weight::from_parts(100_000_000, 0)
}

fn reserve_withdraw_assets() -> Weight {
Weight::from_parts(100_000_000, 0)
}

fn execute() -> Weight {
Weight::from_parts(100_000_000, 0)
}
Expand Down Expand Up @@ -1124,6 +1129,117 @@ pub mod pallet {
XcmExecutionSuspended::<T>::set(suspended);
Ok(())
}

/// Burn some reserve-based `assets` from local chain and forward a notification XCM to
/// `dest` reserve chain to transfer equivalent assets from reserve to `beneficiary`.
///
/// Fee payment on the destination side is made from the asset in the `assets` vector of
/// index `fee_asset_item`. The weight limit for fees is not provided and thus is unlimited,
/// with all fees taken as needed from the asset.
///
/// - `origin`: Must be capable of withdrawing the `assets` and executing XCM.
/// - `dest`: Destination context for the assets. Will typically be `(Parent,
/// Parachain(..))` to claim reserve assets from a sibling parachain, or `(Parent,)` to
/// claim the reserve assets from the Relay chain.
/// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will
/// generally be an `AccountId32` value.
/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
/// fee on the `dest` side.
/// - `fee_asset_item`: The index into `assets` of the item which should be used to pay
/// fees.
#[pallet::call_index(11)]
#[pallet::weight({
let maybe_assets: Result<MultiAssets, ()> = (*assets.clone()).try_into();
let maybe_dest: Result<MultiLocation, ()> = (*dest.clone()).try_into();
match (maybe_assets, maybe_dest) {
(Ok(assets), Ok(dest)) => {
use sp_std::vec;
let all_assets = AllCounted(assets.len() as u32);
let mut message = Xcm(vec![
WithdrawAsset(assets),
InitiateReserveWithdraw { assets: Wild(all_assets), reserve: dest, xcm: Xcm(vec![]) },
]);
T::Weigher::weight(&mut message).map_or(
Weight::MAX,
|w| T::WeightInfo::reserve_withdraw_assets().saturating_add(w)
)
}
_ => Weight::MAX,
}
})]
pub fn reserve_withdraw_assets(
origin: OriginFor<T>,
dest: Box<VersionedMultiLocation>,
beneficiary: Box<VersionedMultiLocation>,
assets: Box<VersionedMultiAssets>,
fee_asset_item: u32,
) -> DispatchResult {
Self::do_reserve_withdraw_assets(
origin,
dest,
beneficiary,
assets,
fee_asset_item,
None,
)
}

/// Burn some reserve-based `assets` from local chain and forward a notification XCM to
/// `dest` reserve chain to transfer equivalent assets from reserve to `beneficiary`.
///
/// Fee payment on the destination side is made from the asset in the `assets` vector of
/// index `fee_asset_item`, up to enough to pay for `weight_limit` of weight. If more weight
/// is needed than `weight_limit`, then the operation will fail and the assets send may be
/// at risk.
///
/// - `origin`: Must be capable of withdrawing the `assets` and executing XCM.
/// - `dest`: Destination context for the assets. Will typically be `(Parent,
/// Parachain(..))` to claim reserve assets from a sibling parachain, or `(Parent,)` to
/// claim the reserve assets from the Relay chain.
/// - `beneficiary`: A beneficiary location for the assets in the context of `dest`. Will
/// generally be an `AccountId32` value.
/// - `assets`: The assets to be withdrawn. This should include the assets used to pay the
/// fee on the `dest` side.
/// - `fee_asset_item`: The index into `assets` of the item which should be used to pay
/// fees.
/// - `weight_limit`: The remote-side weight limit, if any, for the XCM fee purchase.
#[pallet::call_index(12)]
#[pallet::weight({
let maybe_assets: Result<MultiAssets, ()> = (*assets.clone()).try_into();
let maybe_dest: Result<MultiLocation, ()> = (*dest.clone()).try_into();
match (maybe_assets, maybe_dest) {
(Ok(assets), Ok(dest)) => {
use sp_std::vec;
let all_assets = AllCounted(assets.len() as u32);
let mut message = Xcm(vec![
WithdrawAsset(assets),
InitiateReserveWithdraw { assets: Wild(all_assets), reserve: dest, xcm: Xcm(vec![]) },
]);
T::Weigher::weight(&mut message).map_or(
Weight::MAX,
|w| T::WeightInfo::reserve_withdraw_assets().saturating_add(w)
)
}
_ => Weight::MAX,
}
})]
pub fn limited_reserve_withdraw_assets(
origin: OriginFor<T>,
dest: Box<VersionedMultiLocation>,
beneficiary: Box<VersionedMultiLocation>,
assets: Box<VersionedMultiAssets>,
fee_asset_item: u32,
weight_limit: WeightLimit,
) -> DispatchResult {
Self::do_reserve_withdraw_assets(
origin,
dest,
beneficiary,
assets,
fee_asset_item,
Some(weight_limit),
)
}
}
}

Expand Down Expand Up @@ -1251,6 +1367,65 @@ impl<T: Config> Pallet<T> {
Ok(())
}

fn do_reserve_withdraw_assets(
origin: OriginFor<T>,
dest: Box<VersionedMultiLocation>,
beneficiary: Box<VersionedMultiLocation>,
assets: Box<VersionedMultiAssets>,
fee_asset_item: u32,
maybe_weight_limit: Option<WeightLimit>,
) -> DispatchResult {
let origin_location = T::ExecuteXcmOrigin::ensure_origin(origin)?;
let dest = (*dest).try_into().map_err(|()| Error::<T>::BadVersion)?;
let beneficiary: MultiLocation =
(*beneficiary).try_into().map_err(|()| Error::<T>::BadVersion)?;
let assets: MultiAssets = (*assets).try_into().map_err(|()| Error::<T>::BadVersion)?;

ensure!(assets.len() <= MAX_ASSETS_FOR_TRANSFER, Error::<T>::TooManyAssets);
let value = (origin_location, assets.into_inner());
let (origin_location, assets) = value;
let context = T::UniversalLocation::get();
let fees = assets
.get(fee_asset_item as usize)
.ok_or(Error::<T>::Empty)?
.clone()
.reanchored(&dest, context)
.map_err(|_| Error::<T>::CannotReanchor)?;
let max_assets = assets.len() as u32;
let assets: MultiAssets = assets.into();
let weight_limit = match maybe_weight_limit {
Some(weight_limit) => weight_limit,
None => {
let fees = fees.clone();
let mut remote_message = Xcm(vec![
WithdrawAsset(assets.clone()),
ClearOrigin,
BuyExecution { fees, weight_limit: Limited(Weight::zero()) },
DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary },
]);
// use local weight for remote message and hope for the best.
let remote_weight = T::Weigher::weight(&mut remote_message)
.map_err(|()| Error::<T>::UnweighableMessage)?;
Limited(remote_weight)
},
};
let xcm = Xcm(vec![
BuyExecution { fees, weight_limit },
DepositAsset { assets: Wild(AllCounted(max_assets)), beneficiary },
]);
let mut message = Xcm(vec![
WithdrawAsset(assets),
InitiateReserveWithdraw { assets: Wild(AllCounted(max_assets)), reserve: dest, xcm },
]);
let weight =
T::Weigher::weight(&mut message).map_err(|()| Error::<T>::UnweighableMessage)?;
let hash = message.using_encoded(sp_io::hashing::blake2_256);
let outcome =
T::XcmExecutor::execute_xcm_in_credit(origin_location, message, hash, weight, weight);
Self::deposit_event(Event::Attempted { outcome });
Ok(())
}

fn do_teleport_assets(
origin: OriginFor<T>,
dest: Box<VersionedMultiLocation>,
Expand Down
47 changes: 47 additions & 0 deletions polkadot/xcm/pallet-xcm/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,53 @@ fn unlimited_reserve_transfer_assets_works() {
});
}

/// Test `limited_reserve_withdraw_assets` with unlimited weight purchasing
///
/// Asserts that the sender's balance is decreased and the beneficiary's balance
/// is increased. Verifies the correct message is sent and event is emitted.
#[test]
fn unlimited_reserve_withdraw_assets_works() {
let balances = vec![
(ALICE, INITIAL_BALANCE),
(ParaId::from(PARA_ID).into_account_truncating(), INITIAL_BALANCE),
];
new_test_ext_with_balances(balances).execute_with(|| {
let weight = BaseXcmWeight::get() * 2;
let dest: MultiLocation = Junction::AccountId32 { network: None, id: ALICE.into() }.into();
assert_eq!(Balances::total_balance(&ALICE), INITIAL_BALANCE);
// TODO: for a more accurate test, use derivative/synthetic (pallet_assets) asset instead of
// native (pallet_balances) `Here` asset, but mock doesn't have `pallet_assets` and not sure
// it's worth adding just for this.
assert_ok!(XcmPallet::limited_reserve_withdraw_assets(
RuntimeOrigin::signed(ALICE),
Box::new(Parachain(PARA_ID).into()),
Box::new(dest.into()),
Box::new((Here, SEND_AMOUNT).into()),
0,
WeightLimit::Unlimited,
));
// Alice spent amount
assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE - SEND_AMOUNT);
// Check destination XCM program
assert_eq!(
sent_xcm(),
vec![(
Parachain(PARA_ID).into(),
Xcm(vec![
WithdrawAsset((Parent, SEND_AMOUNT).into()),
ClearOrigin,
buy_execution((Parent, SEND_AMOUNT)),
DepositAsset { assets: AllCounted(1).into(), beneficiary: dest },
]),
)]
);
assert_eq!(
last_event(),
RuntimeEvent::XcmPallet(crate::Event::Attempted { outcome: Outcome::Complete(weight) })
);
});
}

/// Test local execution of XCM
///
/// Asserts that the sender's balance is decreased and the beneficiary's balance
Expand Down

0 comments on commit 0b21a04

Please sign in to comment.