XCM WeightTrader: Swap Fee Asset for Native Asset (#1845)

Implements an XCM executor `WeightTrader`, facilitating fee payments in
any asset that can be exchanged for a native asset.

A few constraints need to be observed:
- `buy_weight` and `refund` operations must be atomic, as another weight
trader implementation might be attempted in case of failure.
- swap credit must be utilized since there isn’t an account to which an
asset of some class can be deposited with a guarantee to meet the
existential deposit requirement. Also, operating with credits enhances
the efficiency of the weight trader -
https://github.com/paritytech/polkadot-sdk/pull/1677

related PRs:
- (depends) https://github.com/paritytech/polkadot-sdk/pull/2031
- (depends) https://github.com/paritytech/polkadot-sdk/pull/1677
- (caused) https://github.com/paritytech/polkadot-sdk/pull/1847
- (caused) https://github.com/paritytech/polkadot-sdk/pull/1876

// DONE: impl `OnUnbalanced` for a `fungible/s` credit
// DONE: make the trader free from a concept of a native currency and
drop few fallible conversions. related issue -
https://github.com/paritytech/polkadot-sdk/issues/1842
// DONE: tests

---------

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>
This commit is contained in:
Muharem
2024-01-16 15:34:48 +08:00
committed by GitHub
parent 4c4963a192
commit 2cb39f8dc9
25 changed files with 1769 additions and 861 deletions
@@ -27,78 +27,3 @@ fn send_transact_as_superuser_from_relay_to_system_para_works() {
Some(Weight::from_parts(1_019_445_000, 200_000)),
)
}
/// Parachain should be able to send XCM paying its fee with sufficient asset
/// in the System Parachain
#[test]
fn send_xcm_from_para_to_system_para_paying_fee_with_assets_works() {
let para_sovereign_account = AssetHubRococo::sovereign_account_id_of(
AssetHubRococo::sibling_location_of(PenpalA::para_id()),
);
// Force create and mint assets for Parachain's sovereign account
AssetHubRococo::force_create_and_mint_asset(
ASSET_ID,
ASSET_MIN_BALANCE,
true,
para_sovereign_account.clone(),
Some(Weight::from_parts(1_019_445_000, 200_000)),
ASSET_MIN_BALANCE * 1000000000,
);
// We just need a call that can pass the `SafeCallFilter`
// Call values are not relevant
let call = AssetHubRococo::force_create_asset_call(
ASSET_ID,
para_sovereign_account.clone(),
true,
ASSET_MIN_BALANCE,
);
let origin_kind = OriginKind::SovereignAccount;
let fee_amount = ASSET_MIN_BALANCE * 1000000;
let native_asset =
(X2(PalletInstance(ASSETS_PALLET_ID), GeneralIndex(ASSET_ID.into())), fee_amount).into();
let root_origin = <PenpalA as Chain>::RuntimeOrigin::root();
let system_para_destination = PenpalA::sibling_location_of(AssetHubRococo::para_id()).into();
let xcm = xcm_transact_paid_execution(
call,
origin_kind,
native_asset,
para_sovereign_account.clone(),
);
PenpalA::execute_with(|| {
assert_ok!(<PenpalA as PenpalAPallet>::PolkadotXcm::send(
root_origin,
bx!(system_para_destination),
bx!(xcm),
));
PenpalA::assert_xcm_pallet_sent();
});
AssetHubRococo::execute_with(|| {
type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
AssetHubRococo::assert_xcmp_queue_success(Some(Weight::from_parts(
15_594_564_000,
562_893,
)));
assert_expected_events!(
AssetHubRococo,
vec![
RuntimeEvent::Assets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
asset_id: *asset_id == ASSET_ID,
owner: *owner == para_sovereign_account,
balance: *balance == fee_amount,
},
RuntimeEvent::Assets(pallet_assets::Event::Issued { asset_id, .. }) => {
asset_id: *asset_id == ASSET_ID,
},
]
);
});
}
@@ -270,3 +270,130 @@ fn cannot_create_pool_from_pool_assets() {
);
});
}
#[test]
fn pay_xcm_fee_with_some_asset_swapped_for_native() {
let asset_native = asset_hub_rococo_runtime::xcm_config::TokenLocation::get();
let asset_one = MultiLocation {
parents: 0,
interior: X2(PalletInstance(ASSETS_PALLET_ID), GeneralIndex(ASSET_ID.into())),
};
let penpal = AssetHubRococo::sovereign_account_id_of(AssetHubRococo::sibling_location_of(
PenpalA::para_id(),
));
AssetHubRococo::execute_with(|| {
type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
// set up pool with ASSET_ID <> NATIVE pair
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::Assets::create(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
ASSET_ID.into(),
AssetHubRococoSender::get().into(),
ASSET_MIN_BALANCE,
));
assert!(<AssetHubRococo as AssetHubRococoPallet>::Assets::asset_exists(ASSET_ID));
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::Assets::mint(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
ASSET_ID.into(),
AssetHubRococoSender::get().into(),
3_000_000_000_000,
));
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::create_pool(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
Box::new(asset_native),
Box::new(asset_one),
));
assert_expected_events!(
AssetHubRococo,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {},
]
);
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::add_liquidity(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
Box::new(asset_native),
Box::new(asset_one),
1_000_000_000_000,
2_000_000_000_000,
0,
0,
AssetHubRococoSender::get().into()
));
assert_expected_events!(
AssetHubRococo,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded {lp_token_minted, .. }) => { lp_token_minted: *lp_token_minted == 1414213562273, },
]
);
// ensure `penpal` sovereign account has no native tokens and mint some `ASSET_ID`
assert_eq!(
<AssetHubRococo as AssetHubRococoPallet>::Balances::free_balance(penpal.clone()),
0
);
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::Assets::touch_other(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
ASSET_ID.into(),
penpal.clone().into(),
));
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::Assets::mint(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
ASSET_ID.into(),
penpal.clone().into(),
10_000_000_000_000,
));
});
PenpalA::execute_with(|| {
// send xcm transact from `penpal` account which as only `ASSET_ID` tokens on
// `AssetHubRococo`
let call = AssetHubRococo::force_create_asset_call(
ASSET_ID + 1000,
penpal.clone(),
true,
ASSET_MIN_BALANCE,
);
let penpal_root = <PenpalA as Chain>::RuntimeOrigin::root();
let fee_amount = 4_000_000_000_000u128;
let asset_one =
(X2(PalletInstance(ASSETS_PALLET_ID), GeneralIndex(ASSET_ID.into())), fee_amount)
.into();
let asset_hub_location = PenpalA::sibling_location_of(AssetHubRococo::para_id()).into();
let xcm = xcm_transact_paid_execution(
call,
OriginKind::SovereignAccount,
asset_one,
penpal.clone(),
);
assert_ok!(<PenpalA as PenpalAPallet>::PolkadotXcm::send(
penpal_root,
bx!(asset_hub_location),
bx!(xcm),
));
PenpalA::assert_xcm_pallet_sent();
});
AssetHubRococo::execute_with(|| {
type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
AssetHubRococo::assert_xcmp_queue_success(None);
assert_expected_events!(
AssetHubRococo,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::SwapCreditExecuted { .. },) => {},
RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed { success: true,.. }) => {},
]
);
});
}
@@ -27,78 +27,3 @@ fn send_transact_as_superuser_from_relay_to_system_para_works() {
Some(Weight::from_parts(1_019_445_000, 200_000)),
)
}
/// Parachain should be able to send XCM paying its fee with sufficient asset
/// in the System Parachain
#[test]
fn send_xcm_from_para_to_system_para_paying_fee_with_assets_works() {
let para_sovereign_account = AssetHubWestend::sovereign_account_id_of(
AssetHubWestend::sibling_location_of(PenpalB::para_id()),
);
// Force create and mint assets for Parachain's sovereign account
AssetHubWestend::force_create_and_mint_asset(
ASSET_ID,
ASSET_MIN_BALANCE,
true,
para_sovereign_account.clone(),
Some(Weight::from_parts(1_019_445_000, 200_000)),
ASSET_MIN_BALANCE * 1000000000,
);
// We just need a call that can pass the `SafeCallFilter`
// Call values are not relevant
let call = AssetHubWestend::force_create_asset_call(
ASSET_ID,
para_sovereign_account.clone(),
true,
ASSET_MIN_BALANCE,
);
let origin_kind = OriginKind::SovereignAccount;
let fee_amount = ASSET_MIN_BALANCE * 1000000;
let native_asset =
(X2(PalletInstance(ASSETS_PALLET_ID), GeneralIndex(ASSET_ID.into())), fee_amount).into();
let root_origin = <PenpalB as Chain>::RuntimeOrigin::root();
let system_para_destination = PenpalB::sibling_location_of(AssetHubWestend::para_id()).into();
let xcm = xcm_transact_paid_execution(
call,
origin_kind,
native_asset,
para_sovereign_account.clone(),
);
PenpalB::execute_with(|| {
assert_ok!(<PenpalB as PenpalBPallet>::PolkadotXcm::send(
root_origin,
bx!(system_para_destination),
bx!(xcm),
));
PenpalB::assert_xcm_pallet_sent();
});
AssetHubWestend::execute_with(|| {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
AssetHubWestend::assert_xcmp_queue_success(Some(Weight::from_parts(
16_290_336_000,
562_893,
)));
assert_expected_events!(
AssetHubWestend,
vec![
RuntimeEvent::Assets(pallet_assets::Event::Burned { asset_id, owner, balance }) => {
asset_id: *asset_id == ASSET_ID,
owner: *owner == para_sovereign_account,
balance: *balance == fee_amount,
},
RuntimeEvent::Assets(pallet_assets::Event::Issued { asset_id, .. }) => {
asset_id: *asset_id == ASSET_ID,
},
]
);
});
}
@@ -264,3 +264,130 @@ fn cannot_create_pool_from_pool_assets() {
);
});
}
#[test]
fn pay_xcm_fee_with_some_asset_swapped_for_native() {
let asset_native = asset_hub_westend_runtime::xcm_config::WestendLocation::get();
let asset_one = MultiLocation {
parents: 0,
interior: X2(PalletInstance(ASSETS_PALLET_ID), GeneralIndex(ASSET_ID.into())),
};
let penpal = AssetHubWestend::sovereign_account_id_of(AssetHubWestend::sibling_location_of(
PenpalB::para_id(),
));
AssetHubWestend::execute_with(|| {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
// set up pool with ASSET_ID <> NATIVE pair
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::Assets::create(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
ASSET_ID.into(),
AssetHubWestendSender::get().into(),
ASSET_MIN_BALANCE,
));
assert!(<AssetHubWestend as AssetHubWestendPallet>::Assets::asset_exists(ASSET_ID));
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::Assets::mint(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
ASSET_ID.into(),
AssetHubWestendSender::get().into(),
3_000_000_000_000,
));
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::create_pool(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
Box::new(asset_native),
Box::new(asset_one),
));
assert_expected_events!(
AssetHubWestend,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {},
]
);
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::add_liquidity(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
Box::new(asset_native),
Box::new(asset_one),
1_000_000_000_000,
2_000_000_000_000,
0,
0,
AssetHubWestendSender::get().into()
));
assert_expected_events!(
AssetHubWestend,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded {lp_token_minted, .. }) => { lp_token_minted: *lp_token_minted == 1414213562273, },
]
);
// ensure `penpal` sovereign account has no native tokens and mint some `ASSET_ID`
assert_eq!(
<AssetHubWestend as AssetHubWestendPallet>::Balances::free_balance(penpal.clone()),
0
);
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::Assets::touch_other(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
ASSET_ID.into(),
penpal.clone().into(),
));
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::Assets::mint(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
ASSET_ID.into(),
penpal.clone().into(),
10_000_000_000_000,
));
});
PenpalB::execute_with(|| {
// send xcm transact from `penpal` account which as only `ASSET_ID` tokens on
// `AssetHubWestend`
let call = AssetHubWestend::force_create_asset_call(
ASSET_ID + 1000,
penpal.clone(),
true,
ASSET_MIN_BALANCE,
);
let penpal_root = <PenpalB as Chain>::RuntimeOrigin::root();
let fee_amount = 4_000_000_000_000u128;
let asset_one =
(X2(PalletInstance(ASSETS_PALLET_ID), GeneralIndex(ASSET_ID.into())), fee_amount)
.into();
let asset_hub_location = PenpalB::sibling_location_of(AssetHubWestend::para_id()).into();
let xcm = xcm_transact_paid_execution(
call,
OriginKind::SovereignAccount,
asset_one,
penpal.clone(),
);
assert_ok!(<PenpalB as PenpalBPallet>::PolkadotXcm::send(
penpal_root,
bx!(asset_hub_location),
bx!(xcm),
));
PenpalB::assert_xcm_pallet_sent();
});
AssetHubWestend::execute_with(|| {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
AssetHubWestend::assert_xcmp_queue_success(None);
assert_expected_events!(
AssetHubWestend,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::SwapCreditExecuted { .. },) => {},
RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed { success: true,.. }) => {},
]
);
});
}
@@ -20,6 +20,7 @@ hex-literal = "0.4.1"
sp-core = { path = "../../../../../../../substrate/primitives/core", default-features = false }
frame-support = { path = "../../../../../../../substrate/frame/support", default-features = false }
pallet-assets = { path = "../../../../../../../substrate/frame/assets", default-features = false }
pallet-asset-conversion = { path = "../../../../../../../substrate/frame/asset-conversion", default-features = false }
pallet-balances = { path = "../../../../../../../substrate/frame/balances", default-features = false }
pallet-message-queue = { path = "../../../../../../../substrate/frame/message-queue" }
sp-runtime = { path = "../../../../../../../substrate/primitives/runtime", default-features = false }
@@ -61,7 +61,8 @@ pub use rococo_westend_system_emulated_network::{
rococo_emulated_chain::{genesis::ED as ROCOCO_ED, RococoRelayPallet as RococoPallet},
AssetHubRococoPara as AssetHubRococo, AssetHubRococoParaReceiver as AssetHubRococoReceiver,
AssetHubRococoParaSender as AssetHubRococoSender, AssetHubWestendPara as AssetHubWestend,
AssetHubWestendParaReceiver as AssetHubWestendReceiver, BridgeHubRococoPara as BridgeHubRococo,
AssetHubWestendParaReceiver as AssetHubWestendReceiver,
AssetHubWestendParaSender as AssetHubWestendSender, BridgeHubRococoPara as BridgeHubRococo,
BridgeHubRococoParaSender as BridgeHubRococoSender, BridgeHubWestendPara as BridgeHubWestend,
RococoRelay as Rococo, RococoRelayReceiver as RococoReceiver,
RococoRelaySender as RococoSender,
@@ -49,6 +49,49 @@ fn send_rocs_from_asset_hub_rococo_to_asset_hub_westend() {
AssetHubWestend::para_id(),
);
AssetHubWestend::execute_with(|| {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
// setup a pool to pay xcm fees with `roc_at_asset_hub_westend` tokens
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::ForeignAssets::mint(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
roc_at_asset_hub_westend.into(),
AssetHubWestendSender::get().into(),
3_000_000_000_000,
));
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::create_pool(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
Box::new(Parent.into()),
Box::new(roc_at_asset_hub_westend),
));
assert_expected_events!(
AssetHubWestend,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {},
]
);
assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::AssetConversion::add_liquidity(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendSender::get()),
Box::new(Parent.into()),
Box::new(roc_at_asset_hub_westend),
1_000_000_000_000,
2_000_000_000_000,
1,
1,
AssetHubWestendSender::get().into()
));
assert_expected_events!(
AssetHubWestend,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded {..}) => {},
]
);
});
let rocs_in_reserve_on_ahr_before =
<AssetHubRococo as Chain>::account_data_of(sov_ahw_on_ahr.clone()).free;
let sender_rocs_before =
@@ -58,7 +101,7 @@ fn send_rocs_from_asset_hub_rococo_to_asset_hub_westend() {
<Assets as Inspect<_>>::balance(roc_at_asset_hub_westend, &AssetHubWestendReceiver::get())
});
let amount = ASSET_HUB_ROCOCO_ED * 1_000;
let amount = ASSET_HUB_ROCOCO_ED * 1_000_000;
send_asset_from_asset_hub_rococo_to_asset_hub_westend(roc_at_asset_hub_rococo, amount);
AssetHubWestend::execute_with(|| {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
@@ -16,6 +16,7 @@ codec = { package = "parity-scale-codec", version = "3.4.0", default-features =
# Substrate
frame-support = { path = "../../../../../../../substrate/frame/support", default-features = false }
pallet-assets = { path = "../../../../../../../substrate/frame/assets", default-features = false }
pallet-asset-conversion = { path = "../../../../../../../substrate/frame/asset-conversion", default-features = false }
pallet-balances = { path = "../../../../../../../substrate/frame/balances", default-features = false }
pallet-message-queue = { path = "../../../../../../../substrate/frame/message-queue" }
sp-runtime = { path = "../../../../../../../substrate/primitives/runtime", default-features = false }
@@ -55,7 +55,8 @@ pub use rococo_westend_system_emulated_network::{
},
westend_emulated_chain::WestendRelayPallet as WestendPallet,
AssetHubRococoPara as AssetHubRococo, AssetHubRococoParaReceiver as AssetHubRococoReceiver,
AssetHubWestendPara as AssetHubWestend, AssetHubWestendParaReceiver as AssetHubWestendReceiver,
AssetHubRococoParaSender as AssetHubRococoSender, AssetHubWestendPara as AssetHubWestend,
AssetHubWestendParaReceiver as AssetHubWestendReceiver,
AssetHubWestendParaSender as AssetHubWestendSender, BridgeHubRococoPara as BridgeHubRococo,
BridgeHubWestendPara as BridgeHubWestend, BridgeHubWestendParaSender as BridgeHubWestendSender,
WestendRelay as Westend,
@@ -48,6 +48,49 @@ fn send_wnds_from_asset_hub_westend_to_asset_hub_rococo() {
AssetHubRococo::para_id(),
);
AssetHubRococo::execute_with(|| {
type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
// setup a pool to pay xcm fees with `wnd_at_asset_hub_rococo` tokens
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::ForeignAssets::mint(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
wnd_at_asset_hub_rococo.into(),
AssetHubRococoSender::get().into(),
3_000_000_000_000,
));
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::create_pool(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
Box::new(Parent.into()),
Box::new(wnd_at_asset_hub_rococo),
));
assert_expected_events!(
AssetHubRococo,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {},
]
);
assert_ok!(<AssetHubRococo as AssetHubRococoPallet>::AssetConversion::add_liquidity(
<AssetHubRococo as Chain>::RuntimeOrigin::signed(AssetHubRococoSender::get()),
Box::new(Parent.into()),
Box::new(wnd_at_asset_hub_rococo),
1_000_000_000_000,
2_000_000_000_000,
1,
1,
AssetHubRococoSender::get().into()
));
assert_expected_events!(
AssetHubRococo,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded {..}) => {},
]
);
});
let wnds_in_reserve_on_ahw_before =
<AssetHubWestend as Chain>::account_data_of(sov_ahr_on_ahw.clone()).free;
let sender_wnds_before =
@@ -312,7 +312,7 @@ impl pallet_assets::Config<PoolAssetsInstance> for Runtime {
type BenchmarkHelper = ();
}
/// Union fungibles implementation for `Assets`` and `ForeignAssets`.
/// Union fungibles implementation for `Assets` and `ForeignAssets`.
pub type LocalAndForeignAssets = fungibles::UnionOf<
Assets,
ForeignAssets,
@@ -324,18 +324,21 @@ pub type LocalAndForeignAssets = fungibles::UnionOf<
AccountId,
>;
/// Union fungibles implementation for [`LocalAndForeignAssets`] and `Balances`.
pub type NativeAndAssets = fungible::UnionOf<
Balances,
LocalAndForeignAssets,
TargetFromLeft<TokenLocation>,
MultiLocation,
AccountId,
>;
impl pallet_asset_conversion::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = Balance;
type HigherPrecisionBalance = sp_core::U256;
type AssetKind = MultiLocation;
type Assets = fungible::UnionOf<
Balances,
LocalAndForeignAssets,
TargetFromLeft<TokenLocation>,
Self::AssetKind,
Self::AccountId,
>;
type Assets = NativeAndAssets;
type PoolId = (Self::AssetKind, Self::AssetKind);
type PoolLocator =
pallet_asset_conversion::WithFirstAsset<TokenLocation, AccountId, Self::AssetKind>;
@@ -15,17 +15,21 @@
use super::{
AccountId, AllPalletsWithSystem, Assets, Authorship, Balance, Balances, BaseDeliveryFee,
FeeAssetId, ForeignAssets, ForeignAssetsInstance, ParachainInfo, ParachainSystem, PolkadotXcm,
PoolAssets, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, ToWestendXcmRouter,
TransactionByteFee, TrustBackedAssetsInstance, WeightToFee, XcmpQueue,
CollatorSelection, FeeAssetId, ForeignAssets, ForeignAssetsInstance, ParachainInfo,
ParachainSystem, PolkadotXcm, PoolAssets, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin,
ToWestendXcmRouter, TransactionByteFee, TrustBackedAssetsInstance, WeightToFee, XcmpQueue,
};
use assets_common::{
local_and_foreign_assets::MatchesLocalAndForeignAssetsMultiLocation,
matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset},
TrustBackedAssetsAsMultiLocation,
};
use frame_support::{
match_types, parameter_types,
traits::{ConstU32, Contains, Equals, Everything, Nothing, PalletInfoAccess},
traits::{
tokens::imbalance::ResolveAssetTo, ConstU32, Contains, Equals, Everything, Nothing,
PalletInfoAccess,
},
};
use frame_system::EnsureRoot;
use pallet_xcm::XcmPassthrough;
@@ -73,6 +77,7 @@ parameter_types! {
pub PoolAssetsPalletLocation: MultiLocation =
PalletInstance(<PoolAssets as PalletInfoAccess>::index() as u8).into();
pub CheckingAccount: AccountId = PolkadotXcm::check_account();
pub StakingPot: AccountId = CollatorSelection::account_id();
pub const GovernanceLocation: MultiLocation = MultiLocation::parent();
pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating();
pub RelayTreasuryLocation: MultiLocation = (Parent, PalletInstance(rococo_runtime_constants::TREASURY_PALLET_ID)).into();
@@ -550,31 +555,17 @@ impl xcm_executor::Config for XcmConfig {
>;
type Trader = (
UsingComponents<WeightToFee, TokenLocation, AccountId, Balances, ToStakingPot<Runtime>>,
// This trader allows to pay with `is_sufficient=true` "Trust Backed" assets from dedicated
// `pallet_assets` instance - `Assets`.
cumulus_primitives_utility::TakeFirstAssetTrader<
cumulus_primitives_utility::SwapFirstAssetTrader<
TokenLocation,
crate::AssetConversion,
WeightToFee,
crate::NativeAndAssets,
(
TrustBackedAssetsAsMultiLocation<TrustBackedAssetsPalletLocation, Balance>,
ForeignAssetsConvertedConcreteId,
),
ResolveAssetTo<StakingPot, crate::NativeAndAssets>,
AccountId,
AssetFeeAsExistentialDepositMultiplierFeeCharger,
TrustBackedAssetsConvertedConcreteId,
Assets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
FungiblesTransactor,
AccountId,
XcmAssetFeesReceiver,
>,
>,
// This trader allows to pay with `is_sufficient=true` "Foreign" assets from dedicated
// `pallet_assets` instance - `ForeignAssets`.
cumulus_primitives_utility::TakeFirstAssetTrader<
AccountId,
ForeignAssetFeeAsExistentialDepositMultiplierFeeCharger,
ForeignAssetsConvertedConcreteId,
ForeignAssets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
ForeignFungiblesTransactor,
AccountId,
XcmAssetFeesReceiver,
>,
>,
);
type ResponseHandler = PolkadotXcm;
@@ -17,34 +17,38 @@
//! Tests for the Rococo Assets Hub chain.
use asset_hub_rococo_runtime::xcm_config::{
AssetFeeAsExistentialDepositMultiplierFeeCharger, TokenLocation,
TrustBackedAssetsPalletLocation,
use asset_hub_rococo_runtime::{
xcm_config,
xcm_config::{bridging, ForeignCreatorsSovereignAccountOf, LocationToAccountId, TokenLocation},
AllPalletsWithoutSystem, MetadataDepositBase, MetadataDepositPerByte, RuntimeCall,
RuntimeEvent, ToWestendXcmRouterInstance, XcmpQueue,
};
pub use asset_hub_rococo_runtime::{
xcm_config::{
self, bridging, CheckingAccount, ForeignCreatorsSovereignAccountOf, LocationToAccountId,
XcmConfig,
},
AllPalletsWithoutSystem, AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets,
ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem, Runtime,
RuntimeCall, RuntimeEvent, SessionKeys, System, ToWestendXcmRouterInstance,
TrustBackedAssetsInstance, XcmpQueue,
xcm_config::{CheckingAccount, TrustBackedAssetsPalletLocation, XcmConfig},
AssetConversion, AssetDeposit, Assets, Balances, CollatorSelection, ExistentialDeposit,
ForeignAssets, ForeignAssetsInstance, ParachainSystem, Runtime, SessionKeys, System,
TrustBackedAssetsInstance,
};
use asset_test_utils::{
test_cases_over_bridge::TestBridgingConfig, CollatorSessionKey, CollatorSessionKeys, ExtBuilder,
};
use codec::{Decode, Encode};
use cumulus_primitives_utility::ChargeWeightInFungibles;
use frame_support::{
assert_noop, assert_ok,
traits::fungibles::InspectEnumerable,
assert_ok,
traits::{
fungible::{Inspect, Mutate},
fungibles::{
Create, Inspect as FungiblesInspect, InspectEnumerable, Mutate as FungiblesMutate,
},
},
weights::{Weight, WeightToFee as WeightToFeeT},
};
use parachains_common::{
rococo::fee::WeightToFee, AccountId, AssetIdForTrustBackedAssets, AuraId, Balance,
rococo::{currency::UNITS, fee::WeightToFee},
AccountId, AssetIdForTrustBackedAssets, AuraId, Balance,
};
use sp_runtime::traits::MaybeEquivalence;
use std::convert::Into;
use xcm::latest::prelude::*;
use xcm_executor::traits::{Identity, JustTry, WeightTrader};
@@ -69,7 +73,7 @@ fn collator_session_keys() -> CollatorSessionKeys<Runtime> {
}
#[test]
fn test_asset_xcm_trader() {
fn test_buy_and_refund_weight_in_native() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
@@ -79,77 +83,55 @@ fn test_asset_xcm_trader() {
)])
.build()
.execute_with(|| {
// We need root origin to create a sufficient asset
let minimum_asset_balance = 3333333_u128;
let local_asset_id = 1;
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
local_asset_id.into(),
AccountId::from(ALICE).into(),
true,
minimum_asset_balance
));
let bob: AccountId = SOME_ASSET_ADMIN.into();
let staking_pot = CollatorSelection::account_id();
let native_location = TokenLocation::get();
let initial_balance = 200 * UNITS;
// We first mint enough asset for the account to exist for assets
assert_ok!(Assets::mint(
RuntimeHelper::origin_of(AccountId::from(ALICE)),
local_asset_id.into(),
AccountId::from(ALICE).into(),
minimum_asset_balance
));
assert_ok!(Balances::mint_into(&bob, initial_balance));
assert_ok!(Balances::mint_into(&staking_pot, initial_balance));
// get asset id as multilocation
let asset_multilocation =
AssetIdForTrustBackedAssetsConvert::convert_back(&local_asset_id).unwrap();
// keep initial total issuance to assert later.
let total_issuance = Balances::total_issuance();
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// We are going to buy 4e9 weight
let bought = Weight::from_parts(4_000_000_000u64, 0);
// Lets calculate amount needed
let asset_amount_needed =
AssetFeeAsExistentialDepositMultiplierFeeCharger::charge_weight_in_fungibles(
local_asset_id,
bought,
)
.expect("failed to compute");
// Lets pay with: asset_amount_needed + asset_amount_extra
let asset_amount_extra = 100_u128;
let asset: MultiAsset =
(asset_multilocation, asset_amount_needed + asset_amount_extra).into();
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
// prepare input to buy weight.
let weight = Weight::from_parts(4_000_000_000, 0);
let fee = WeightToFee::weight_to_fee(&weight);
let extra_amount = 100;
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
let payment: MultiAsset = (native_location, fee + extra_amount).into();
// Lets buy_weight and make sure buy_weight does not return an error
let unused_assets = trader.buy_weight(bought, asset.into(), &ctx).expect("Expected Ok");
// Check whether a correct amount of unused assets is returned
assert_ok!(
unused_assets.ensure_contains(&(asset_multilocation, asset_amount_extra).into())
);
// init trader and buy weight.
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let unused_asset =
trader.buy_weight(weight, payment.into(), &ctx).expect("Expected Ok");
// Drop trader
// assert.
let unused_amount =
unused_asset.fungible.get(&native_location.into()).map_or(0, |a| *a);
assert_eq!(unused_amount, extra_amount);
assert_eq!(Balances::total_issuance(), total_issuance);
// prepare input to refund weight.
let refund_weight = Weight::from_parts(1_000_000_000, 0);
let refund = WeightToFee::weight_to_fee(&refund_weight);
// refund.
let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
assert_eq!(actual_refund, (native_location, refund).into());
// assert.
assert_eq!(Balances::balance(&staking_pot), initial_balance);
// only after `trader` is dropped we expect the fee to be resolved into the treasury
// account.
drop(trader);
// Make sure author(Alice) has received the amount
assert_eq!(
Assets::balance(local_asset_id, AccountId::from(ALICE)),
minimum_asset_balance + asset_amount_needed
);
// We also need to ensure the total supply increased
assert_eq!(
Assets::total_supply(local_asset_id),
minimum_asset_balance + asset_amount_needed
);
});
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
assert_eq!(Balances::total_issuance(), total_issuance + fee - refund);
})
}
#[test]
fn test_asset_xcm_trader_with_refund() {
fn test_buy_and_refund_weight_with_swap_local_asset_xcm_trader() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
@@ -159,249 +141,192 @@ fn test_asset_xcm_trader_with_refund() {
)])
.build()
.execute_with(|| {
// We need root origin to create a sufficient asset
// We set existential deposit to be identical to the one for Balances first
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
1.into(),
AccountId::from(ALICE).into(),
true,
ExistentialDeposit::get()
let bob: AccountId = SOME_ASSET_ADMIN.into();
let staking_pot = CollatorSelection::account_id();
let asset_1: u32 = 1;
let native_location = TokenLocation::get();
let asset_1_location =
AssetIdForTrustBackedAssetsConvert::convert_back(&asset_1).unwrap();
// bob's initial balance for native and `asset1` assets.
let initial_balance = 200 * UNITS;
// liquidity for both arms of (native, asset1) pool.
let pool_liquidity = 100 * UNITS;
// init asset, balances and pool.
assert_ok!(<Assets as Create<_>>::create(asset_1, bob.clone(), true, 10));
assert_ok!(Assets::mint_into(asset_1, &bob, initial_balance));
assert_ok!(Balances::mint_into(&bob, initial_balance));
assert_ok!(Balances::mint_into(&staking_pot, initial_balance));
assert_ok!(AssetConversion::create_pool(
RuntimeHelper::origin_of(bob.clone()),
Box::new(native_location),
Box::new(asset_1_location)
));
// We first mint enough asset for the account to exist for assets
assert_ok!(Assets::mint(
RuntimeHelper::origin_of(AccountId::from(ALICE)),
1.into(),
AccountId::from(ALICE).into(),
ExistentialDeposit::get()
assert_ok!(AssetConversion::add_liquidity(
RuntimeHelper::origin_of(bob.clone()),
Box::new(native_location),
Box::new(asset_1_location),
pool_liquidity,
pool_liquidity,
1,
1,
bob,
));
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
// keep initial total issuance to assert later.
let asset_total_issuance = Assets::total_issuance(asset_1);
let native_total_issuance = Balances::total_issuance();
// prepare input to buy weight.
let weight = Weight::from_parts(4_000_000_000, 0);
let fee = WeightToFee::weight_to_fee(&weight);
let asset_fee =
AssetConversion::get_amount_in(&fee, &pool_liquidity, &pool_liquidity).unwrap();
let extra_amount = 100;
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
let payment: MultiAsset = (asset_1_location, asset_fee + extra_amount).into();
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// init trader and buy weight.
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let unused_asset =
trader.buy_weight(weight, payment.into(), &ctx).expect("Expected Ok");
// We are going to buy 4e9 weight
let bought = Weight::from_parts(4_000_000_000u64, 0);
// assert.
let unused_amount =
unused_asset.fungible.get(&asset_1_location.into()).map_or(0, |a| *a);
assert_eq!(unused_amount, extra_amount);
assert_eq!(Assets::total_issuance(asset_1), asset_total_issuance + asset_fee);
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap();
// prepare input to refund weight.
let refund_weight = Weight::from_parts(1_000_000_000, 0);
let refund = WeightToFee::weight_to_fee(&refund_weight);
let (reserve1, reserve2) =
AssetConversion::get_reserves(native_location, asset_1_location).unwrap();
let asset_refund =
AssetConversion::get_amount_out(&refund, &reserve1, &reserve2).unwrap();
// lets calculate amount needed
let amount_bought = WeightToFee::weight_to_fee(&bought);
let asset: MultiAsset = (asset_multilocation, amount_bought).into();
// Make sure buy_weight does not return an error
assert_ok!(trader.buy_weight(bought, asset.clone().into(), &ctx));
// Make sure again buy_weight does return an error
// This assert relies on the fact, that we use `TakeFirstAssetTrader` in `WeightTrader`
// tuple chain, which cannot be called twice
assert_noop!(trader.buy_weight(bought, asset.into(), &ctx), XcmError::TooExpensive);
// We actually use half of the weight
let weight_used = bought / 2;
// Make sure refurnd works.
let amount_refunded = WeightToFee::weight_to_fee(&(bought - weight_used));
// refund.
let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
assert_eq!(actual_refund, (asset_1_location, asset_refund).into());
// assert.
assert_eq!(Balances::balance(&staking_pot), initial_balance);
// only after `trader` is dropped we expect the fee to be resolved into the treasury
// account.
drop(trader);
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
assert_eq!(
trader.refund_weight(bought - weight_used, &ctx),
Some((asset_multilocation, amount_refunded).into())
Assets::total_issuance(asset_1),
asset_total_issuance + asset_fee - asset_refund
);
assert_eq!(Balances::total_issuance(), native_total_issuance);
})
}
// Drop trader
drop(trader);
#[test]
fn test_buy_and_refund_weight_with_swap_foreign_asset_xcm_trader() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
AccountId::from(ALICE),
AccountId::from(ALICE),
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
)])
.build()
.execute_with(|| {
let bob: AccountId = SOME_ASSET_ADMIN.into();
let staking_pot = CollatorSelection::account_id();
let native_location = TokenLocation::get();
let foreign_location =
MultiLocation { parents: 1, interior: X2(Parachain(1234), GeneralIndex(12345)) };
// bob's initial balance for native and `asset1` assets.
let initial_balance = 200 * UNITS;
// liquidity for both arms of (native, asset1) pool.
let pool_liquidity = 100 * UNITS;
// We only should have paid for half of the bought weight
let fees_paid = WeightToFee::weight_to_fee(&weight_used);
// init asset, balances and pool.
assert_ok!(<ForeignAssets as Create<_>>::create(
foreign_location,
bob.clone(),
true,
10
));
assert_ok!(ForeignAssets::mint_into(foreign_location, &bob, initial_balance));
assert_ok!(Balances::mint_into(&bob, initial_balance));
assert_ok!(Balances::mint_into(&staking_pot, initial_balance));
assert_ok!(AssetConversion::create_pool(
RuntimeHelper::origin_of(bob.clone()),
Box::new(native_location),
Box::new(foreign_location)
));
assert_ok!(AssetConversion::add_liquidity(
RuntimeHelper::origin_of(bob.clone()),
Box::new(native_location),
Box::new(foreign_location),
pool_liquidity,
pool_liquidity,
1,
1,
bob,
));
// keep initial total issuance to assert later.
let asset_total_issuance = ForeignAssets::total_issuance(foreign_location);
let native_total_issuance = Balances::total_issuance();
// prepare input to buy weight.
let weight = Weight::from_parts(4_000_000_000, 0);
let fee = WeightToFee::weight_to_fee(&weight);
let asset_fee =
AssetConversion::get_amount_in(&fee, &pool_liquidity, &pool_liquidity).unwrap();
let extra_amount = 100;
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
let payment: MultiAsset = (foreign_location, asset_fee + extra_amount).into();
// init trader and buy weight.
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let unused_asset =
trader.buy_weight(weight, payment.into(), &ctx).expect("Expected Ok");
// assert.
let unused_amount =
unused_asset.fungible.get(&foreign_location.into()).map_or(0, |a| *a);
assert_eq!(unused_amount, extra_amount);
assert_eq!(
Assets::balance(1, AccountId::from(ALICE)),
ExistentialDeposit::get() + fees_paid
ForeignAssets::total_issuance(foreign_location),
asset_total_issuance + asset_fee
);
// We also need to ensure the total supply increased
assert_eq!(Assets::total_supply(1), ExistentialDeposit::get() + fees_paid);
});
}
// prepare input to refund weight.
let refund_weight = Weight::from_parts(1_000_000_000, 0);
let refund = WeightToFee::weight_to_fee(&refund_weight);
let (reserve1, reserve2) =
AssetConversion::get_reserves(native_location, foreign_location).unwrap();
let asset_refund =
AssetConversion::get_amount_out(&refund, &reserve1, &reserve2).unwrap();
#[test]
fn test_asset_xcm_trader_refund_not_possible_since_amount_less_than_ed() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
AccountId::from(ALICE),
AccountId::from(ALICE),
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
)])
.build()
.execute_with(|| {
// We need root origin to create a sufficient asset
// We set existential deposit to be identical to the one for Balances first
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
1.into(),
AccountId::from(ALICE).into(),
true,
ExistentialDeposit::get()
));
// refund.
let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
assert_eq!(actual_refund, (foreign_location, asset_refund).into());
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// We are going to buy small amount
let bought = Weight::from_parts(500_000_000u64, 0);
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap();
let amount_bought = WeightToFee::weight_to_fee(&bought);
assert!(
amount_bought < ExistentialDeposit::get(),
"we are testing what happens when the amount does not exceed ED"
);
let asset: MultiAsset = (asset_multilocation, amount_bought).into();
// Buy weight should return an error
assert_noop!(trader.buy_weight(bought, asset.into(), &ctx), XcmError::TooExpensive);
// not credited since the ED is higher than this value
assert_eq!(Assets::balance(1, AccountId::from(ALICE)), 0);
// We also need to ensure the total supply did not increase
assert_eq!(Assets::total_supply(1), 0);
});
}
#[test]
fn test_that_buying_ed_refund_does_not_refund() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
AccountId::from(ALICE),
AccountId::from(ALICE),
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
)])
.build()
.execute_with(|| {
// We need root origin to create a sufficient asset
// We set existential deposit to be identical to the one for Balances first
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
1.into(),
AccountId::from(ALICE).into(),
true,
ExistentialDeposit::get()
));
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// We are gonna buy ED
let bought = Weight::from_parts(ExistentialDeposit::get().try_into().unwrap(), 0);
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap();
let amount_bought = WeightToFee::weight_to_fee(&bought);
assert!(
amount_bought < ExistentialDeposit::get(),
"we are testing what happens when the amount does not exceed ED"
);
// We know we will have to buy at least ED, so lets make sure first it will
// fail with a payment of less than ED
let asset: MultiAsset = (asset_multilocation, amount_bought).into();
assert_noop!(trader.buy_weight(bought, asset.into(), &ctx), XcmError::TooExpensive);
// Now lets buy ED at least
let asset: MultiAsset = (asset_multilocation, ExistentialDeposit::get()).into();
// Buy weight should work
assert_ok!(trader.buy_weight(bought, asset.into(), &ctx));
// Should return None. We have a specific check making sure we dont go below ED for
// drop payment
assert_eq!(trader.refund_weight(bought, &ctx), None);
// Drop trader
// assert.
assert_eq!(Balances::balance(&staking_pot), initial_balance);
// only after `trader` is dropped we expect the fee to be resolved into the treasury
// account.
drop(trader);
// Make sure author(Alice) has received the amount
assert_eq!(Assets::balance(1, AccountId::from(ALICE)), ExistentialDeposit::get());
// We also need to ensure the total supply increased
assert_eq!(Assets::total_supply(1), ExistentialDeposit::get());
});
}
#[test]
fn test_asset_xcm_trader_not_possible_for_non_sufficient_assets() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
AccountId::from(ALICE),
AccountId::from(ALICE),
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
)])
.build()
.execute_with(|| {
// Create a non-sufficient asset with specific existential deposit
let minimum_asset_balance = 1_000_000_u128;
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
1.into(),
AccountId::from(ALICE).into(),
false,
minimum_asset_balance
));
// We first mint enough asset for the account to exist for assets
assert_ok!(Assets::mint(
RuntimeHelper::origin_of(AccountId::from(ALICE)),
1.into(),
AccountId::from(ALICE).into(),
minimum_asset_balance
));
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// We are going to buy 4e9 weight
let bought = Weight::from_parts(4_000_000_000u64, 0);
// lets calculate amount needed
let asset_amount_needed = WeightToFee::weight_to_fee(&bought);
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap();
let asset: MultiAsset = (asset_multilocation, asset_amount_needed).into();
// Make sure again buy_weight does return an error
assert_noop!(trader.buy_weight(bought, asset.into(), &ctx), XcmError::TooExpensive);
// Drop trader
drop(trader);
// Make sure author(Alice) has NOT received the amount
assert_eq!(Assets::balance(1, AccountId::from(ALICE)), minimum_asset_balance);
// We also need to ensure the total supply NOT increased
assert_eq!(Assets::total_supply(1), minimum_asset_balance);
});
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
assert_eq!(
ForeignAssets::total_issuance(foreign_location),
asset_total_issuance + asset_fee - asset_refund
);
assert_eq!(Balances::total_issuance(), native_total_issuance);
})
}
#[test]
@@ -295,7 +295,7 @@ impl pallet_assets::Config<PoolAssetsInstance> for Runtime {
type BenchmarkHelper = ();
}
/// Union fungibles implementation for `Assets`` and `ForeignAssets`.
/// Union fungibles implementation for `Assets` and `ForeignAssets`.
pub type LocalAndForeignAssets = fungibles::UnionOf<
Assets,
ForeignAssets,
@@ -307,18 +307,21 @@ pub type LocalAndForeignAssets = fungibles::UnionOf<
AccountId,
>;
/// Union fungibles implementation for [`LocalAndForeignAssets`] and `Balances`.
pub type NativeAndAssets = fungible::UnionOf<
Balances,
LocalAndForeignAssets,
TargetFromLeft<WestendLocation>,
MultiLocation,
AccountId,
>;
impl pallet_asset_conversion::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = Balance;
type HigherPrecisionBalance = sp_core::U256;
type AssetKind = MultiLocation;
type Assets = fungible::UnionOf<
Balances,
LocalAndForeignAssets,
TargetFromLeft<WestendLocation>,
Self::AssetKind,
Self::AccountId,
>;
type Assets = NativeAndAssets;
type PoolId = (Self::AssetKind, Self::AssetKind);
type PoolLocator =
pallet_asset_conversion::WithFirstAsset<WestendLocation, AccountId, Self::AssetKind>;
@@ -15,17 +15,21 @@
use super::{
AccountId, AllPalletsWithSystem, Assets, Authorship, Balance, Balances, BaseDeliveryFee,
FeeAssetId, ForeignAssets, ForeignAssetsInstance, ParachainInfo, ParachainSystem, PolkadotXcm,
PoolAssets, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, ToRococoXcmRouter,
TransactionByteFee, TrustBackedAssetsInstance, WeightToFee, XcmpQueue,
CollatorSelection, FeeAssetId, ForeignAssets, ForeignAssetsInstance, ParachainInfo,
ParachainSystem, PolkadotXcm, PoolAssets, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin,
ToRococoXcmRouter, TransactionByteFee, TrustBackedAssetsInstance, WeightToFee, XcmpQueue,
};
use assets_common::{
local_and_foreign_assets::MatchesLocalAndForeignAssetsMultiLocation,
matching::{FromSiblingParachain, IsForeignConcreteAsset},
TrustBackedAssetsAsMultiLocation,
};
use frame_support::{
match_types, parameter_types,
traits::{ConstU32, Contains, Equals, Everything, Nothing, PalletInfoAccess},
traits::{
tokens::imbalance::ResolveAssetTo, ConstU32, Contains, Equals, Everything, Nothing,
PalletInfoAccess,
},
};
use frame_system::EnsureRoot;
use pallet_xcm::XcmPassthrough;
@@ -70,6 +74,7 @@ parameter_types! {
pub PoolAssetsPalletLocation: MultiLocation =
PalletInstance(<PoolAssets as PalletInfoAccess>::index() as u8).into();
pub CheckingAccount: AccountId = PolkadotXcm::check_account();
pub StakingPot: AccountId = CollatorSelection::account_id();
pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating();
pub RelayTreasuryLocation: MultiLocation = (Parent, PalletInstance(westend_runtime_constants::TREASURY_PALLET_ID)).into();
}
@@ -567,31 +572,17 @@ impl xcm_executor::Config for XcmConfig {
>;
type Trader = (
UsingComponents<WeightToFee, WestendLocation, AccountId, Balances, ToStakingPot<Runtime>>,
// This trader allows to pay with `is_sufficient=true` "Trust Backed" assets from dedicated
// `pallet_assets` instance - `Assets`.
cumulus_primitives_utility::TakeFirstAssetTrader<
cumulus_primitives_utility::SwapFirstAssetTrader<
WestendLocation,
crate::AssetConversion,
WeightToFee,
crate::NativeAndAssets,
(
TrustBackedAssetsAsMultiLocation<TrustBackedAssetsPalletLocation, Balance>,
ForeignAssetsConvertedConcreteId,
),
ResolveAssetTo<StakingPot, crate::NativeAndAssets>,
AccountId,
AssetFeeAsExistentialDepositMultiplierFeeCharger,
TrustBackedAssetsConvertedConcreteId,
Assets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
FungiblesTransactor,
AccountId,
XcmAssetFeesReceiver,
>,
>,
// This trader allows to pay with `is_sufficient=true` "Foreign" assets from dedicated
// `pallet_assets` instance - `ForeignAssets`.
cumulus_primitives_utility::TakeFirstAssetTrader<
AccountId,
ForeignAssetFeeAsExistentialDepositMultiplierFeeCharger,
ForeignAssetsConvertedConcreteId,
ForeignAssets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
ForeignFungiblesTransactor,
AccountId,
XcmAssetFeesReceiver,
>,
>,
);
type ResponseHandler = PolkadotXcm;
@@ -18,28 +18,36 @@
//! Tests for the Westmint (Westend Assets Hub) chain.
use asset_hub_westend_runtime::{
xcm_config,
xcm_config::{
self, bridging, AssetFeeAsExistentialDepositMultiplierFeeCharger, CheckingAccount,
ForeignCreatorsSovereignAccountOf, LocationToAccountId, TrustBackedAssetsPalletLocation,
WestendLocation, XcmConfig,
bridging, ForeignCreatorsSovereignAccountOf, LocationToAccountId, WestendLocation,
},
AllPalletsWithoutSystem, AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets,
ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem,
PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys,
ToRococoXcmRouterInstance, TrustBackedAssetsInstance, XcmpQueue,
AllPalletsWithoutSystem, MetadataDepositBase, MetadataDepositPerByte, PolkadotXcm, RuntimeCall,
RuntimeEvent, RuntimeOrigin, ToRococoXcmRouterInstance, XcmpQueue,
};
pub use asset_hub_westend_runtime::{
xcm_config::{CheckingAccount, TrustBackedAssetsPalletLocation, XcmConfig},
AssetConversion, AssetDeposit, Assets, Balances, CollatorSelection, ExistentialDeposit,
ForeignAssets, ForeignAssetsInstance, ParachainSystem, Runtime, SessionKeys, System,
TrustBackedAssetsInstance,
};
use asset_test_utils::{
test_cases_over_bridge::TestBridgingConfig, CollatorSessionKey, CollatorSessionKeys, ExtBuilder,
};
use codec::{Decode, Encode};
use cumulus_primitives_utility::ChargeWeightInFungibles;
use frame_support::{
assert_noop, assert_ok,
traits::fungibles::InspectEnumerable,
assert_ok,
traits::{
fungible::{Inspect, Mutate},
fungibles::{
Create, Inspect as FungiblesInspect, InspectEnumerable, Mutate as FungiblesMutate,
},
},
weights::{Weight, WeightToFee as WeightToFeeT},
};
use parachains_common::{
westend::fee::WeightToFee, AccountId, AssetIdForTrustBackedAssets, AuraId, Balance,
westend::{currency::UNITS, fee::WeightToFee},
AccountId, AssetIdForTrustBackedAssets, AuraId, Balance,
};
use sp_runtime::traits::MaybeEquivalence;
use std::convert::Into;
@@ -67,7 +75,7 @@ fn collator_session_keys() -> CollatorSessionKeys<Runtime> {
}
#[test]
fn test_asset_xcm_trader() {
fn test_buy_and_refund_weight_in_native() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
@@ -77,77 +85,55 @@ fn test_asset_xcm_trader() {
)])
.build()
.execute_with(|| {
// We need root origin to create a sufficient asset
let minimum_asset_balance = 3333333_u128;
let local_asset_id = 1;
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
local_asset_id.into(),
AccountId::from(ALICE).into(),
true,
minimum_asset_balance
));
let bob: AccountId = SOME_ASSET_ADMIN.into();
let staking_pot = CollatorSelection::account_id();
let native_location = WestendLocation::get();
let initial_balance = 200 * UNITS;
// We first mint enough asset for the account to exist for assets
assert_ok!(Assets::mint(
RuntimeHelper::origin_of(AccountId::from(ALICE)),
local_asset_id.into(),
AccountId::from(ALICE).into(),
minimum_asset_balance
));
assert_ok!(Balances::mint_into(&bob, initial_balance));
assert_ok!(Balances::mint_into(&staking_pot, initial_balance));
// get asset id as multilocation
let asset_multilocation =
AssetIdForTrustBackedAssetsConvert::convert_back(&local_asset_id).unwrap();
// keep initial total issuance to assert later.
let total_issuance = Balances::total_issuance();
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// We are going to buy 4e9 weight
let bought = Weight::from_parts(4_000_000_000u64, 0);
// Lets calculate amount needed
let asset_amount_needed =
AssetFeeAsExistentialDepositMultiplierFeeCharger::charge_weight_in_fungibles(
local_asset_id,
bought,
)
.expect("failed to compute");
// Lets pay with: asset_amount_needed + asset_amount_extra
let asset_amount_extra = 100_u128;
let asset: MultiAsset =
(asset_multilocation, asset_amount_needed + asset_amount_extra).into();
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
// prepare input to buy weight.
let weight = Weight::from_parts(4_000_000_000, 0);
let fee = WeightToFee::weight_to_fee(&weight);
let extra_amount = 100;
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
let payment: MultiAsset = (native_location, fee + extra_amount).into();
// Lets buy_weight and make sure buy_weight does not return an error
let unused_assets = trader.buy_weight(bought, asset.into(), &ctx).expect("Expected Ok");
// Check whether a correct amount of unused assets is returned
assert_ok!(
unused_assets.ensure_contains(&(asset_multilocation, asset_amount_extra).into())
);
// init trader and buy weight.
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let unused_asset =
trader.buy_weight(weight, payment.into(), &ctx).expect("Expected Ok");
// Drop trader
// assert.
let unused_amount =
unused_asset.fungible.get(&native_location.into()).map_or(0, |a| *a);
assert_eq!(unused_amount, extra_amount);
assert_eq!(Balances::total_issuance(), total_issuance);
// prepare input to refund weight.
let refund_weight = Weight::from_parts(1_000_000_000, 0);
let refund = WeightToFee::weight_to_fee(&refund_weight);
// refund.
let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
assert_eq!(actual_refund, (native_location, refund).into());
// assert.
assert_eq!(Balances::balance(&staking_pot), initial_balance);
// only after `trader` is dropped we expect the fee to be resolved into the treasury
// account.
drop(trader);
// Make sure author(Alice) has received the amount
assert_eq!(
Assets::balance(local_asset_id, AccountId::from(ALICE)),
minimum_asset_balance + asset_amount_needed
);
// We also need to ensure the total supply increased
assert_eq!(
Assets::total_supply(local_asset_id),
minimum_asset_balance + asset_amount_needed
);
});
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
assert_eq!(Balances::total_issuance(), total_issuance + fee - refund);
})
}
#[test]
fn test_asset_xcm_trader_with_refund() {
fn test_buy_and_refund_weight_with_swap_local_asset_xcm_trader() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
@@ -157,247 +143,192 @@ fn test_asset_xcm_trader_with_refund() {
)])
.build()
.execute_with(|| {
// We need root origin to create a sufficient asset
// We set existential deposit to be identical to the one for Balances first
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
1.into(),
AccountId::from(ALICE).into(),
true,
ExistentialDeposit::get()
let bob: AccountId = SOME_ASSET_ADMIN.into();
let staking_pot = CollatorSelection::account_id();
let asset_1: u32 = 1;
let native_location = WestendLocation::get();
let asset_1_location =
AssetIdForTrustBackedAssetsConvert::convert_back(&asset_1).unwrap();
// bob's initial balance for native and `asset1` assets.
let initial_balance = 200 * UNITS;
// liquidity for both arms of (native, asset1) pool.
let pool_liquidity = 100 * UNITS;
// init asset, balances and pool.
assert_ok!(<Assets as Create<_>>::create(asset_1, bob.clone(), true, 10));
assert_ok!(Assets::mint_into(asset_1, &bob, initial_balance));
assert_ok!(Balances::mint_into(&bob, initial_balance));
assert_ok!(Balances::mint_into(&staking_pot, initial_balance));
assert_ok!(AssetConversion::create_pool(
RuntimeHelper::origin_of(bob.clone()),
Box::new(native_location),
Box::new(asset_1_location)
));
// We first mint enough asset for the account to exist for assets
assert_ok!(Assets::mint(
RuntimeHelper::origin_of(AccountId::from(ALICE)),
1.into(),
AccountId::from(ALICE).into(),
ExistentialDeposit::get()
assert_ok!(AssetConversion::add_liquidity(
RuntimeHelper::origin_of(bob.clone()),
Box::new(native_location),
Box::new(asset_1_location),
pool_liquidity,
pool_liquidity,
1,
1,
bob,
));
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
// keep initial total issuance to assert later.
let asset_total_issuance = Assets::total_issuance(asset_1);
let native_total_issuance = Balances::total_issuance();
// prepare input to buy weight.
let weight = Weight::from_parts(4_000_000_000, 0);
let fee = WeightToFee::weight_to_fee(&weight);
let asset_fee =
AssetConversion::get_amount_in(&fee, &pool_liquidity, &pool_liquidity).unwrap();
let extra_amount = 100;
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
let payment: MultiAsset = (asset_1_location, asset_fee + extra_amount).into();
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// init trader and buy weight.
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let unused_asset =
trader.buy_weight(weight, payment.into(), &ctx).expect("Expected Ok");
// We are going to buy 4e9 weight
let bought = Weight::from_parts(4_000_000_000u64, 0);
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap();
// assert.
let unused_amount =
unused_asset.fungible.get(&asset_1_location.into()).map_or(0, |a| *a);
assert_eq!(unused_amount, extra_amount);
assert_eq!(Assets::total_issuance(asset_1), asset_total_issuance + asset_fee);
// lets calculate amount needed
let amount_bought = WeightToFee::weight_to_fee(&bought);
// prepare input to refund weight.
let refund_weight = Weight::from_parts(1_000_000_000, 0);
let refund = WeightToFee::weight_to_fee(&refund_weight);
let (reserve1, reserve2) =
AssetConversion::get_reserves(native_location, asset_1_location).unwrap();
let asset_refund =
AssetConversion::get_amount_out(&refund, &reserve1, &reserve2).unwrap();
let asset: MultiAsset = (asset_multilocation, amount_bought).into();
// Make sure buy_weight does not return an error
assert_ok!(trader.buy_weight(bought, asset.clone().into(), &ctx));
// Make sure again buy_weight does return an error
// This assert relies on the fact, that we use `TakeFirstAssetTrader` in `WeightTrader`
// tuple chain, which cannot be called twice
assert_noop!(trader.buy_weight(bought, asset.into(), &ctx), XcmError::TooExpensive);
// We actually use half of the weight
let weight_used = bought / 2;
// Make sure refurnd works.
let amount_refunded = WeightToFee::weight_to_fee(&(bought - weight_used));
// refund.
let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
assert_eq!(actual_refund, (asset_1_location, asset_refund).into());
// assert.
assert_eq!(Balances::balance(&staking_pot), initial_balance);
// only after `trader` is dropped we expect the fee to be resolved into the treasury
// account.
drop(trader);
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
assert_eq!(
trader.refund_weight(bought - weight_used, &ctx),
Some((asset_multilocation, amount_refunded).into())
Assets::total_issuance(asset_1),
asset_total_issuance + asset_fee - asset_refund
);
assert_eq!(Balances::total_issuance(), native_total_issuance);
})
}
// Drop trader
drop(trader);
#[test]
fn test_buy_and_refund_weight_with_swap_foreign_asset_xcm_trader() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
AccountId::from(ALICE),
AccountId::from(ALICE),
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
)])
.build()
.execute_with(|| {
let bob: AccountId = SOME_ASSET_ADMIN.into();
let staking_pot = CollatorSelection::account_id();
let native_location = WestendLocation::get();
let foreign_location =
MultiLocation { parents: 1, interior: X2(Parachain(1234), GeneralIndex(12345)) };
// bob's initial balance for native and `asset1` assets.
let initial_balance = 200 * UNITS;
// liquidity for both arms of (native, asset1) pool.
let pool_liquidity = 100 * UNITS;
// We only should have paid for half of the bought weight
let fees_paid = WeightToFee::weight_to_fee(&weight_used);
// init asset, balances and pool.
assert_ok!(<ForeignAssets as Create<_>>::create(
foreign_location,
bob.clone(),
true,
10
));
assert_ok!(ForeignAssets::mint_into(foreign_location, &bob, initial_balance));
assert_ok!(Balances::mint_into(&bob, initial_balance));
assert_ok!(Balances::mint_into(&staking_pot, initial_balance));
assert_ok!(AssetConversion::create_pool(
RuntimeHelper::origin_of(bob.clone()),
Box::new(native_location),
Box::new(foreign_location)
));
assert_ok!(AssetConversion::add_liquidity(
RuntimeHelper::origin_of(bob.clone()),
Box::new(native_location),
Box::new(foreign_location),
pool_liquidity,
pool_liquidity,
1,
1,
bob,
));
// keep initial total issuance to assert later.
let asset_total_issuance = ForeignAssets::total_issuance(foreign_location);
let native_total_issuance = Balances::total_issuance();
// prepare input to buy weight.
let weight = Weight::from_parts(4_000_000_000, 0);
let fee = WeightToFee::weight_to_fee(&weight);
let asset_fee =
AssetConversion::get_amount_in(&fee, &pool_liquidity, &pool_liquidity).unwrap();
let extra_amount = 100;
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
let payment: MultiAsset = (foreign_location, asset_fee + extra_amount).into();
// init trader and buy weight.
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let unused_asset =
trader.buy_weight(weight, payment.into(), &ctx).expect("Expected Ok");
// assert.
let unused_amount =
unused_asset.fungible.get(&foreign_location.into()).map_or(0, |a| *a);
assert_eq!(unused_amount, extra_amount);
assert_eq!(
Assets::balance(1, AccountId::from(ALICE)),
ExistentialDeposit::get() + fees_paid
ForeignAssets::total_issuance(foreign_location),
asset_total_issuance + asset_fee
);
// We also need to ensure the total supply increased
assert_eq!(Assets::total_supply(1), ExistentialDeposit::get() + fees_paid);
});
}
// prepare input to refund weight.
let refund_weight = Weight::from_parts(1_000_000_000, 0);
let refund = WeightToFee::weight_to_fee(&refund_weight);
let (reserve1, reserve2) =
AssetConversion::get_reserves(native_location, foreign_location).unwrap();
let asset_refund =
AssetConversion::get_amount_out(&refund, &reserve1, &reserve2).unwrap();
#[test]
fn test_asset_xcm_trader_refund_not_possible_since_amount_less_than_ed() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
AccountId::from(ALICE),
AccountId::from(ALICE),
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
)])
.build()
.execute_with(|| {
// We need root origin to create a sufficient asset
// We set existential deposit to be identical to the one for Balances first
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
1.into(),
AccountId::from(ALICE).into(),
true,
ExistentialDeposit::get()
));
// refund.
let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
assert_eq!(actual_refund, (foreign_location, asset_refund).into());
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// We are going to buy small amount
let bought = Weight::from_parts(500_000_000u64, 0);
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap();
let amount_bought = WeightToFee::weight_to_fee(&bought);
assert!(
amount_bought < ExistentialDeposit::get(),
"we are testing what happens when the amount does not exceed ED"
);
let asset: MultiAsset = (asset_multilocation, amount_bought).into();
// Buy weight should return an error
assert_noop!(trader.buy_weight(bought, asset.into(), &ctx), XcmError::TooExpensive);
// not credited since the ED is higher than this value
assert_eq!(Assets::balance(1, AccountId::from(ALICE)), 0);
// We also need to ensure the total supply did not increase
assert_eq!(Assets::total_supply(1), 0);
});
}
#[test]
fn test_that_buying_ed_refund_does_not_refund() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
AccountId::from(ALICE),
AccountId::from(ALICE),
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
)])
.build()
.execute_with(|| {
// We need root origin to create a sufficient asset
// We set existential deposit to be identical to the one for Balances first
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
1.into(),
AccountId::from(ALICE).into(),
true,
ExistentialDeposit::get()
));
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
let bought = Weight::from_parts(500_000_000u64, 0);
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap();
let amount_bought = WeightToFee::weight_to_fee(&bought);
assert!(
amount_bought < ExistentialDeposit::get(),
"we are testing what happens when the amount does not exceed ED"
);
// We know we will have to buy at least ED, so lets make sure first it will
// fail with a payment of less than ED
let asset: MultiAsset = (asset_multilocation, amount_bought).into();
assert_noop!(trader.buy_weight(bought, asset.into(), &ctx), XcmError::TooExpensive);
// Now lets buy ED at least
let asset: MultiAsset = (asset_multilocation, ExistentialDeposit::get()).into();
// Buy weight should work
assert_ok!(trader.buy_weight(bought, asset.into(), &ctx));
// Should return None. We have a specific check making sure we dont go below ED for
// drop payment
assert_eq!(trader.refund_weight(bought, &ctx), None);
// Drop trader
// assert.
assert_eq!(Balances::balance(&staking_pot), initial_balance);
// only after `trader` is dropped we expect the fee to be resolved into the treasury
// account.
drop(trader);
// Make sure author(Alice) has received the amount
assert_eq!(Assets::balance(1, AccountId::from(ALICE)), ExistentialDeposit::get());
// We also need to ensure the total supply increased
assert_eq!(Assets::total_supply(1), ExistentialDeposit::get());
});
}
#[test]
fn test_asset_xcm_trader_not_possible_for_non_sufficient_assets() {
ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![(
AccountId::from(ALICE),
AccountId::from(ALICE),
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
)])
.build()
.execute_with(|| {
// Create a non-sufficient asset with specific existential deposit
let minimum_asset_balance = 1_000_000_u128;
assert_ok!(Assets::force_create(
RuntimeHelper::root_origin(),
1.into(),
AccountId::from(ALICE).into(),
false,
minimum_asset_balance
));
// We first mint enough asset for the account to exist for assets
assert_ok!(Assets::mint(
RuntimeHelper::origin_of(AccountId::from(ALICE)),
1.into(),
AccountId::from(ALICE).into(),
minimum_asset_balance
));
let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
// Set Alice as block author, who will receive fees
RuntimeHelper::run_to_block(2, AccountId::from(ALICE));
// We are going to buy 4e9 weight
let bought = Weight::from_parts(4_000_000_000u64, 0);
// lets calculate amount needed
let asset_amount_needed = WeightToFee::weight_to_fee(&bought);
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap();
let asset: MultiAsset = (asset_multilocation, asset_amount_needed).into();
// Make sure again buy_weight does return an error
assert_noop!(trader.buy_weight(bought, asset.into(), &ctx), XcmError::TooExpensive);
// Drop trader
drop(trader);
// Make sure author(Alice) has NOT received the amount
assert_eq!(Assets::balance(1, AccountId::from(ALICE)), minimum_asset_balance);
// We also need to ensure the total supply NOT increased
assert_eq!(Assets::total_supply(1), minimum_asset_balance);
});
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
assert_eq!(
ForeignAssets::total_issuance(foreign_location),
asset_total_issuance + asset_fee - asset_refund
);
assert_eq!(Balances::total_issuance(), native_total_issuance);
})
}
#[test]
@@ -47,6 +47,16 @@ pub type TrustBackedAssetsConvertedConcreteId<TrustBackedAssetsPalletLocation, B
/// AssetId used for identifying assets by MultiLocation.
pub type MultiLocationForAssetId = MultiLocation;
/// [`MatchedConvertedConcreteId`] converter dedicated for `TrustBackedAssets`
pub type TrustBackedAssetsAsMultiLocation<TrustBackedAssetsPalletLocation, Balance> =
MatchedConvertedConcreteId<
MultiLocationForAssetId,
Balance,
StartsWith<TrustBackedAssetsPalletLocation>,
Identity,
JustTry,
>;
/// [`MatchedConvertedConcreteId`] converter dedicated for storing `AssetId` as `MultiLocation`.
pub type MultiLocationConvertedConcreteId<MultiLocationFilter, Balance> =
MatchedConvertedConcreteId<
@@ -16,6 +16,7 @@ codec = { package = "parity-scale-codec", version = "3.0.0", default-features =
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
pallet-assets = { path = "../../../../../substrate/frame/assets", default-features = false }
pallet-asset-conversion = { path = "../../../../../substrate/frame/asset-conversion", default-features = false }
pallet-balances = { path = "../../../../../substrate/frame/balances", default-features = false }
pallet-session = { path = "../../../../../substrate/frame/session", default-features = false }
sp-consensus-aura = { path = "../../../../../substrate/primitives/consensus/aura", default-features = false }
@@ -64,6 +65,7 @@ std = [
"cumulus-test-relay-sproof-builder/std",
"frame-support/std",
"frame-system/std",
"pallet-asset-conversion/std",
"pallet-assets/std",
"pallet-balances/std",
"pallet-collator-selection/std",
@@ -30,6 +30,7 @@ use parachains_runtimes_test_utils::{
ValidatorIdOf, XcmReceivedFrom,
};
use sp_runtime::{traits::StaticLookup, Saturating};
use sp_std::ops::Mul;
use xcm::{latest::prelude::*, VersionedMultiAssets};
use xcm_builder::{CreateMatcher, MatchXcm};
use xcm_executor::{traits::ConvertLocation, XcmExecutor};
@@ -336,12 +337,13 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
+ pallet_collator_selection::Config
+ cumulus_pallet_parachain_system::Config
+ cumulus_pallet_xcmp_queue::Config
+ pallet_assets::Config<ForeignAssetsPalletInstance>,
+ pallet_assets::Config<ForeignAssetsPalletInstance>
+ pallet_asset_conversion::Config,
AllPalletsWithoutSystem:
OnInitialize<BlockNumberFor<Runtime>> + OnFinalize<BlockNumberFor<Runtime>>,
AccountIdOf<Runtime>: Into<[u8; 32]>,
AccountIdOf<Runtime>: Into<[u8; 32]> + From<[u8; 32]>,
ValidatorIdOf<Runtime>: From<AccountIdOf<Runtime>>,
BalanceOf<Runtime>: From<Balance>,
BalanceOf<Runtime>: From<Balance> + Into<Balance>,
XcmConfig: xcm_executor::Config,
LocationToAccountId: ConvertLocation<AccountIdOf<Runtime>>,
<Runtime as pallet_assets::Config<ForeignAssetsPalletInstance>>::AssetId:
@@ -354,6 +356,9 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
+ Into<AccountId>,
<<Runtime as frame_system::Config>::Lookup as StaticLookup>::Source:
From<<Runtime as frame_system::Config>::AccountId>,
<Runtime as pallet_asset_conversion::Config>::AssetKind:
From<MultiLocation> + Into<MultiLocation>,
<Runtime as pallet_asset_conversion::Config>::Balance: From<Balance>,
ForeignAssetsPalletInstance: 'static,
{
ExtBuilder::<Runtime>::default()
@@ -400,6 +405,43 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
)
);
// setup a pool to pay fees with `foreign_asset_id_multilocation` tokens
let pool_owner: AccountIdOf<Runtime> = [1u8; 32].into();
let native_asset = MultiLocation::parent();
let pool_liquidity: u128 =
existential_deposit.into().max(foreign_asset_id_minimum_balance).mul(100_000);
let _ = <pallet_balances::Pallet<Runtime>>::deposit_creating(
&pool_owner,
(existential_deposit.into() + pool_liquidity).mul(2).into(),
);
assert_ok!(<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::mint(
RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::origin_of(
sovereign_account_as_owner_of_foreign_asset
),
foreign_asset_id_multilocation.into(),
pool_owner.clone().into(),
(foreign_asset_id_minimum_balance + pool_liquidity).mul(2).into(),
));
assert_ok!(<pallet_asset_conversion::Pallet<Runtime>>::create_pool(
RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::origin_of(pool_owner.clone()),
Box::new(native_asset.into()),
Box::new(foreign_asset_id_multilocation.into())
));
assert_ok!(<pallet_asset_conversion::Pallet<Runtime>>::add_liquidity(
RuntimeHelper::<Runtime, AllPalletsWithoutSystem>::origin_of(pool_owner.clone()),
Box::new(native_asset.into()),
Box::new(foreign_asset_id_multilocation.into()),
pool_liquidity.into(),
pool_liquidity.into(),
1.into(),
1.into(),
pool_owner,
));
// Balances before
assert_eq!(
<pallet_balances::Pallet<Runtime>>::free_balance(&target_account),
@@ -485,14 +527,12 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
);
assert_ok!(outcome.ensure_complete());
// author actual balance after (received fees from Trader for ForeignAssets)
let author_received_fees =
<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
foreign_asset_id_multilocation.into(),
&block_author_account,
);
// Balances after (untouched)
// Balances after
// staking pot receives xcm fees in dot
assert!(
<pallet_balances::Pallet<Runtime>>::free_balance(&staking_pot) !=
existential_deposit
);
assert_eq!(
<pallet_balances::Pallet<Runtime>>::free_balance(&target_account),
existential_deposit.clone()
@@ -501,25 +541,13 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
<pallet_balances::Pallet<Runtime>>::free_balance(&block_author_account),
0.into()
);
assert_eq!(
<pallet_balances::Pallet<Runtime>>::free_balance(&staking_pot),
existential_deposit.clone()
);
// ForeignAssets balances after
assert_eq!(
assert!(
<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
foreign_asset_id_multilocation.into(),
&target_account
),
(transfered_foreign_asset_id_amount - author_received_fees.into()).into()
);
assert_eq!(
<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
foreign_asset_id_multilocation.into(),
&block_author_account
),
author_received_fees
) > 0.into()
);
assert_eq!(
<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
@@ -528,6 +556,13 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
),
0.into()
);
assert_eq!(
<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
foreign_asset_id_multilocation.into(),
&block_author_account
),
0.into()
);
})
}