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
Generated
+4
View File
@@ -1066,6 +1066,7 @@ dependencies = [
"frame-support",
"frame-system",
"hex-literal",
"pallet-asset-conversion",
"pallet-assets",
"pallet-balances",
"pallet-collator-selection",
@@ -1973,6 +1974,7 @@ dependencies = [
"frame-support",
"hex",
"hex-literal",
"pallet-asset-conversion",
"pallet-assets",
"pallet-balances",
"pallet-bridge-messages",
@@ -2165,6 +2167,7 @@ dependencies = [
"cumulus-pallet-xcmp-queue",
"emulated-integration-tests-common",
"frame-support",
"pallet-asset-conversion",
"pallet-assets",
"pallet-balances",
"pallet-bridge-messages",
@@ -4095,6 +4098,7 @@ dependencies = [
"cumulus-primitives-core",
"frame-support",
"log",
"pallet-asset-conversion",
"pallet-xcm-benchmarks",
"parity-scale-codec",
"polkadot-runtime-common",
@@ -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<
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,
cumulus_primitives_utility::SwapFirstAssetTrader<
TokenLocation,
crate::AssetConversion,
WeightToFee,
crate::NativeAndAssets,
(
TrustBackedAssetsAsMultiLocation<TrustBackedAssetsPalletLocation, Balance>,
ForeignAssetsConvertedConcreteId,
ForeignAssets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
ForeignFungiblesTransactor,
),
ResolveAssetTo<StakingPot, crate::NativeAndAssets>,
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<
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,
cumulus_primitives_utility::SwapFirstAssetTrader<
WestendLocation,
crate::AssetConversion,
WeightToFee,
crate::NativeAndAssets,
(
TrustBackedAssetsAsMultiLocation<TrustBackedAssetsPalletLocation, Balance>,
ForeignAssetsConvertedConcreteId,
ForeignAssets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
ForeignFungiblesTransactor,
),
ResolveAssetTo<StakingPot, crate::NativeAndAssets>,
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
// staking pot receives xcm fees in dot
assert!(
<pallet_balances::Pallet<Runtime>>::free_balance(&staking_pot) !=
existential_deposit
);
// Balances after (untouched)
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()
);
})
}
+3
View File
@@ -18,6 +18,7 @@ frame-support = { path = "../../../substrate/frame/support", default-features =
sp-io = { path = "../../../substrate/primitives/io", default-features = false }
sp-runtime = { path = "../../../substrate/primitives/runtime", default-features = false }
sp-std = { path = "../../../substrate/primitives/std", default-features = false }
pallet-asset-conversion = { path = "../../../substrate/frame/asset-conversion", default-features = false }
# Polkadot
polkadot-runtime-common = { path = "../../../polkadot/runtime/common", default-features = false }
@@ -37,6 +38,7 @@ std = [
"cumulus-primitives-core/std",
"frame-support/std",
"log/std",
"pallet-asset-conversion/std",
"pallet-xcm-benchmarks/std",
"polkadot-runtime-common/std",
"polkadot-runtime-parachains/std",
@@ -51,6 +53,7 @@ std = [
runtime-benchmarks = [
"cumulus-primitives-core/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"pallet-asset-conversion/runtime-benchmarks",
"pallet-xcm-benchmarks/runtime-benchmarks",
"polkadot-runtime-common/runtime-benchmarks",
"polkadot-runtime-parachains/runtime-benchmarks",
+251 -19
View File
@@ -22,19 +22,24 @@
use codec::Encode;
use cumulus_primitives_core::{MessageSendError, UpwardMessageSender};
use frame_support::{
traits::{
tokens::{fungibles, fungibles::Inspect},
Get,
},
weights::Weight,
defensive,
traits::{tokens::fungibles, Get, OnUnbalanced as OnUnbalancedT},
weights::{Weight, WeightToFee as WeightToFeeT},
};
use pallet_asset_conversion::SwapCredit as SwapCreditT;
use polkadot_runtime_common::xcm_sender::PriceForMessageDelivery;
use sp_runtime::{traits::Saturating, SaturatedConversion};
use sp_runtime::{
traits::{Saturating, Zero},
SaturatedConversion,
};
use sp_std::{marker::PhantomData, prelude::*};
use xcm::{latest::prelude::*, WrapVersion};
use xcm_builder::TakeRevenue;
use xcm_executor::traits::{MatchesFungibles, TransactAsset, WeightTrader};
#[cfg(test)]
mod tests;
/// Xcm router which recognises the `Parent` destination and handles it by sending the message into
/// the given UMP `UpwardMessageSender` implementation. Thus this essentially adapts an
/// `UpwardMessageSender` trait impl into a `SendXcm` trait impl.
@@ -286,23 +291,238 @@ impl<
/// in such assetId for that amount of weight
pub trait ChargeWeightInFungibles<AccountId, Assets: fungibles::Inspect<AccountId>> {
fn charge_weight_in_fungibles(
asset_id: <Assets as Inspect<AccountId>>::AssetId,
asset_id: <Assets as fungibles::Inspect<AccountId>>::AssetId,
weight: Weight,
) -> Result<<Assets as Inspect<AccountId>>::Balance, XcmError>;
) -> Result<<Assets as fungibles::Inspect<AccountId>>::Balance, XcmError>;
}
/// Provides an implementation of [`WeightTrader`] to charge for weight using the first asset
/// specified in the `payment` argument.
///
/// The asset used to pay for the weight must differ from the `Target` asset and be exchangeable for
/// the same `Target` asset through `SwapCredit`.
///
/// ### Parameters:
/// - `Target`: the asset into which the user's payment will be exchanged using `SwapCredit`.
/// - `SwapCredit`: mechanism used for the exchange of the user's payment asset into the `Target`.
/// - `WeightToFee`: weight to the `Target` asset fee calculator.
/// - `Fungibles`: registry of fungible assets.
/// - `FungiblesAssetMatcher`: utility for mapping [`MultiAsset`] to `Fungibles::AssetId` and
/// `Fungibles::Balance`.
/// - `OnUnbalanced`: handler for the fee payment.
/// - `AccountId`: the account identifier type.
pub struct SwapFirstAssetTrader<
Target: Get<Fungibles::AssetId>,
SwapCredit: SwapCreditT<
AccountId,
Balance = Fungibles::Balance,
AssetKind = Fungibles::AssetId,
Credit = fungibles::Credit<AccountId, Fungibles>,
>,
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
Fungibles: fungibles::Balanced<AccountId>,
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
AccountId,
> where
Fungibles::Balance: Into<u128>,
{
/// Accumulated fee paid for XCM execution.
total_fee: fungibles::Credit<AccountId, Fungibles>,
/// Last asset utilized by a client to settle a fee.
last_fee_asset: Option<AssetId>,
_phantom_data: PhantomData<(
Target,
SwapCredit,
WeightToFee,
Fungibles,
FungiblesAssetMatcher,
OnUnbalanced,
AccountId,
)>,
}
impl<
Target: Get<Fungibles::AssetId>,
SwapCredit: SwapCreditT<
AccountId,
Balance = Fungibles::Balance,
AssetKind = Fungibles::AssetId,
Credit = fungibles::Credit<AccountId, Fungibles>,
>,
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
Fungibles: fungibles::Balanced<AccountId>,
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
AccountId,
> WeightTrader
for SwapFirstAssetTrader<
Target,
SwapCredit,
WeightToFee,
Fungibles,
FungiblesAssetMatcher,
OnUnbalanced,
AccountId,
> where
Fungibles::Balance: Into<u128>,
{
fn new() -> Self {
Self {
total_fee: fungibles::Credit::<AccountId, Fungibles>::zero(Target::get()),
last_fee_asset: None,
_phantom_data: PhantomData,
}
}
fn buy_weight(
&mut self,
weight: Weight,
mut payment: xcm_executor::Assets,
_context: &XcmContext,
) -> Result<xcm_executor::Assets, XcmError> {
log::trace!(
target: "xcm::weight",
"SwapFirstAssetTrader::buy_weight weight: {:?}, payment: {:?}",
weight,
payment,
);
let first_asset: MultiAsset =
payment.fungible.pop_first().ok_or(XcmError::AssetNotFound)?.into();
let (fungibles_asset, balance) = FungiblesAssetMatcher::matches_fungibles(&first_asset)
.map_err(|_| XcmError::AssetNotFound)?;
let swap_asset = fungibles_asset.clone().into();
if Target::get().eq(&swap_asset) {
// current trader is not applicable.
return Err(XcmError::FeesNotMet)
}
let credit_in = Fungibles::issue(fungibles_asset, balance);
let fee = WeightToFee::weight_to_fee(&weight);
// swap the user's asset for the `Target` asset.
let (credit_out, credit_change) = SwapCredit::swap_tokens_for_exact_tokens(
vec![swap_asset, Target::get()],
credit_in,
fee,
)
.map_err(|(credit_in, _)| {
drop(credit_in);
XcmError::FeesNotMet
})?;
match self.total_fee.subsume(credit_out) {
Err(credit_out) => {
// error may occur if `total_fee.asset` differs from `credit_out.asset`, which does
// not apply in this context.
defensive!(
"`total_fee.asset` must be equal to `credit_out.asset`",
(self.total_fee.asset(), credit_out.asset())
);
return Err(XcmError::FeesNotMet)
},
_ => (),
};
self.last_fee_asset = Some(first_asset.id);
payment.fungible.insert(first_asset.id, credit_change.peek().into());
drop(credit_change);
Ok(payment)
}
fn refund_weight(&mut self, weight: Weight, _context: &XcmContext) -> Option<MultiAsset> {
log::trace!(
target: "xcm::weight",
"SwapFirstAssetTrader::refund_weight weight: {:?}, self.total_fee: {:?}",
weight,
self.total_fee,
);
if self.total_fee.peek().is_zero() {
// noting yet paid to refund.
return None
}
let mut refund_asset = if let Some(asset) = &self.last_fee_asset {
// create an initial zero refund in the asset used in the last `buy_weight`.
(*asset, Fungible(0)).into()
} else {
return None
};
let refund_amount = WeightToFee::weight_to_fee(&weight);
if refund_amount >= self.total_fee.peek() {
// not enough was paid to refund the `weight`.
return None
}
let refund_swap_asset = FungiblesAssetMatcher::matches_fungibles(&refund_asset)
.map(|(a, _)| a.into())
.ok()?;
let refund = self.total_fee.extract(refund_amount);
let refund = match SwapCredit::swap_exact_tokens_for_tokens(
vec![Target::get(), refund_swap_asset],
refund,
None,
) {
Ok(refund_in_target) => refund_in_target,
Err((refund, _)) => {
// return an attempted refund back to the `total_fee`.
let _ = self.total_fee.subsume(refund).map_err(|refund| {
// error may occur if `total_fee.asset` differs from `refund.asset`, which does
// not apply in this context.
defensive!(
"`total_fee.asset` must be equal to `refund.asset`",
(self.total_fee.asset(), refund.asset())
);
});
return None
},
};
refund_asset.fun = refund.peek().into().into();
drop(refund);
Some(refund_asset)
}
}
impl<
Target: Get<Fungibles::AssetId>,
SwapCredit: SwapCreditT<
AccountId,
Balance = Fungibles::Balance,
AssetKind = Fungibles::AssetId,
Credit = fungibles::Credit<AccountId, Fungibles>,
>,
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
Fungibles: fungibles::Balanced<AccountId>,
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
AccountId,
> Drop
for SwapFirstAssetTrader<
Target,
SwapCredit,
WeightToFee,
Fungibles,
FungiblesAssetMatcher,
OnUnbalanced,
AccountId,
> where
Fungibles::Balance: Into<u128>,
{
fn drop(&mut self) {
if self.total_fee.peek().is_zero() {
return
}
let total_fee = self.total_fee.extract(self.total_fee.peek());
OnUnbalanced::on_unbalanced(total_fee);
}
}
#[cfg(test)]
mod tests {
mod test_xcm_router {
use super::*;
use cumulus_primitives_core::UpwardMessage;
use frame_support::{
assert_ok,
traits::tokens::{
DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence,
},
};
use sp_runtime::DispatchError;
use xcm_executor::{traits::Error, Assets};
/// Validates [`validate`] for required Some(destination) and Some(message)
struct OkFixedXcmHashWithAssertingRequiredInputsSender;
@@ -398,6 +618,18 @@ mod tests {
)>(dest.into(), message)
);
}
}
#[cfg(test)]
mod test_trader {
use super::*;
use frame_support::{
assert_ok,
traits::tokens::{
DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence,
},
};
use sp_runtime::DispatchError;
use xcm_executor::{traits::Error, Assets};
#[test]
fn take_first_asset_trader_buy_weight_called_twice_throws_error() {
@@ -491,9 +723,9 @@ mod tests {
struct FeeChargerAssetsHandleRefund;
impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund {
fn charge_weight_in_fungibles(
_: <TestAssets as Inspect<TestAccountId>>::AssetId,
_: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
_: Weight,
) -> Result<<TestAssets as Inspect<TestAccountId>>::Balance, XcmError> {
) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
Ok(AMOUNT)
}
}
@@ -0,0 +1,17 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Cumulus.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Cumulus. If not, see <http://www.gnu.org/licenses/>.
mod swap_first;
@@ -0,0 +1,551 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Cumulus.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Cumulus. If not, see <http://www.gnu.org/licenses/>.
use crate::*;
use frame_support::{parameter_types, traits::fungibles::Inspect};
use mock::{setup_pool, AccountId, AssetId, Balance, Fungibles};
use xcm::latest::AssetId as XcmAssetId;
use xcm_executor::Assets as HoldingAsset;
fn create_holding_asset(asset_id: AssetId, amount: Balance) -> HoldingAsset {
create_asset(asset_id, amount).into()
}
fn create_asset(asset_id: AssetId, amount: Balance) -> MultiAsset {
MultiAsset { id: create_asset_id(asset_id), fun: Fungible(amount) }
}
fn create_asset_id(asset_id: AssetId) -> XcmAssetId {
Concrete(MultiLocation::new(0, X1(GeneralIndex(asset_id.into()))))
}
fn xcm_context() -> XcmContext {
XcmContext { origin: None, message_id: [0u8; 32], topic: None }
}
fn weight_worth_of(fee: Balance) -> Weight {
Weight::from_parts(fee.try_into().unwrap(), 0)
}
const TARGET_ASSET: AssetId = 1;
const CLIENT_ASSET: AssetId = 2;
const CLIENT_ASSET_2: AssetId = 3;
parameter_types! {
pub const TargetAsset: AssetId = TARGET_ASSET;
}
pub type Trader = SwapFirstAssetTrader<
TargetAsset,
mock::Swap,
mock::WeightToFee,
mock::Fungibles,
mock::FungiblesMatcher,
(),
AccountId,
>;
#[test]
fn holding_asset_swap_for_target() {
let client_asset_total = 15;
let fee = 5;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
}
#[test]
fn holding_asset_swap_for_target_twice() {
let client_asset_total = 20;
let fee1 = 5;
let fee2 = 6;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change1 = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1);
let holding_change2 = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1 - fee2);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee1), holding_asset, &xcm_context()).unwrap(),
holding_change1
);
assert_eq!(
trader
.buy_weight(weight_worth_of(fee2), holding_change1, &xcm_context())
.unwrap(),
holding_change2
);
assert_eq!(trader.total_fee.peek(), fee1 + fee2);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee1 + fee2);
}
#[test]
fn buy_and_refund_twice_for_target() {
let client_asset_total = 15;
let fee = 5;
let refund1 = 4;
let refund2 = 2;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
// create pool for refund swap.
setup_pool(TARGET_ASSET, 1000, CLIENT_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
let refund_asset = create_asset(CLIENT_ASSET, refund1);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(trader.refund_weight(weight_worth_of(refund1), &xcm_context()), Some(refund_asset));
assert_eq!(trader.total_fee.peek(), fee - refund1);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(trader.refund_weight(weight_worth_of(refund2), &xcm_context()), None);
assert_eq!(trader.total_fee.peek(), fee - refund1);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee - refund1);
}
#[test]
fn buy_with_various_assets_and_refund_for_target() {
let client_asset_total = 10;
let client_asset_2_total = 15;
let fee1 = 5;
let fee2 = 6;
let refund1 = 6;
let refund2 = 4;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
setup_pool(CLIENT_ASSET_2, 1000, TARGET_ASSET, 1000);
// create pool for refund swap.
setup_pool(TARGET_ASSET, 1000, CLIENT_ASSET_2, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_asset_2 = create_holding_asset(CLIENT_ASSET_2, client_asset_2_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1);
let holding_change_2 = create_holding_asset(CLIENT_ASSET_2, client_asset_2_total - fee2);
// both refunds in the latest buy asset (`CLIENT_ASSET_2`).
let refund_asset = create_asset(CLIENT_ASSET_2, refund1);
let refund_asset_2 = create_asset(CLIENT_ASSET_2, refund2);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let client_total_2 = Fungibles::total_issuance(CLIENT_ASSET_2);
let mut trader = Trader::new();
// first purchase with `CLIENT_ASSET`.
assert_eq!(
trader.buy_weight(weight_worth_of(fee1), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee1);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
// second purchase with `CLIENT_ASSET_2`.
assert_eq!(
trader
.buy_weight(weight_worth_of(fee2), holding_asset_2, &xcm_context())
.unwrap(),
holding_change_2
);
assert_eq!(trader.total_fee.peek(), fee1 + fee2);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
// first refund in the last asset used with `buy_weight`.
assert_eq!(trader.refund_weight(weight_worth_of(refund1), &xcm_context()), Some(refund_asset));
assert_eq!(trader.total_fee.peek(), fee1 + fee2 - refund1);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
// second refund in the last asset used with `buy_weight`.
assert_eq!(
trader.refund_weight(weight_worth_of(refund2), &xcm_context()),
Some(refund_asset_2)
);
assert_eq!(trader.total_fee.peek(), fee1 + fee2 - refund1 - refund2);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee1);
assert_eq!(
Fungibles::total_issuance(CLIENT_ASSET_2),
client_total_2 + fee2 - refund1 - refund2
);
}
#[test]
fn not_enough_to_refund() {
let client_asset_total = 15;
let fee = 5;
let refund = 6;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(trader.refund_weight(weight_worth_of(refund), &xcm_context()), None);
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
}
#[test]
fn not_exchangeable_to_refund() {
let client_asset_total = 15;
let fee = 5;
let refund = 1;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(trader.refund_weight(weight_worth_of(refund), &xcm_context()), None);
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
}
#[test]
fn nothing_to_refund() {
let fee = 5;
let mut trader = Trader::new();
assert_eq!(trader.refund_weight(weight_worth_of(fee), &xcm_context()), None);
}
#[test]
fn holding_asset_not_exchangeable_for_target() {
let holding_asset = create_holding_asset(CLIENT_ASSET, 10);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader
.buy_weight(Weight::from_all(10), holding_asset, &xcm_context())
.unwrap_err(),
XcmError::FeesNotMet
);
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total);
}
#[test]
fn empty_holding_asset() {
let mut trader = Trader::new();
assert_eq!(
trader
.buy_weight(Weight::from_all(10), HoldingAsset::new(), &xcm_context())
.unwrap_err(),
XcmError::AssetNotFound
);
}
#[test]
fn fails_to_match_holding_asset() {
let mut trader = Trader::new();
let holding_asset =
MultiAsset { id: Concrete(MultiLocation::new(1, X1(Parachain(1)))), fun: Fungible(10) };
assert_eq!(
trader
.buy_weight(Weight::from_all(10), holding_asset.into(), &xcm_context())
.unwrap_err(),
XcmError::AssetNotFound
);
}
#[test]
fn holding_asset_equal_to_target_asset() {
let mut trader = Trader::new();
let holding_asset = create_holding_asset(TargetAsset::get(), 10);
assert_eq!(
trader
.buy_weight(Weight::from_all(10), holding_asset, &xcm_context())
.unwrap_err(),
XcmError::FeesNotMet
);
}
pub mod mock {
use crate::*;
use core::cell::RefCell;
use frame_support::{
ensure,
traits::{
fungibles::{Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Unbalanced},
tokens::{
DepositConsequence, Fortitude, Fortitude::Polite, Precision::Exact, Preservation,
Preservation::Preserve, Provenance, WithdrawConsequence,
},
},
};
use sp_runtime::{traits::One, DispatchError};
use std::collections::HashMap;
use xcm::latest::Junction;
pub type AccountId = u64;
pub type AssetId = u32;
pub type Balance = u128;
pub type Credit = fungibles::Credit<AccountId, Fungibles>;
thread_local! {
pub static TOTAL_ISSUANCE: RefCell<HashMap<AssetId, Balance>> = RefCell::new(HashMap::new());
pub static ACCOUNT: RefCell<HashMap<(AssetId, AccountId), Balance>> = RefCell::new(HashMap::new());
pub static SWAP: RefCell<HashMap<(AssetId, AssetId), AccountId>> = RefCell::new(HashMap::new());
}
pub struct Swap {}
impl SwapCreditT<AccountId> for Swap {
type Balance = Balance;
type AssetKind = AssetId;
type Credit = Credit;
fn max_path_len() -> u32 {
2
}
fn swap_exact_tokens_for_tokens(
path: Vec<Self::AssetKind>,
credit_in: Self::Credit,
amount_out_min: Option<Self::Balance>,
) -> Result<Self::Credit, (Self::Credit, DispatchError)> {
ensure!(2 == path.len(), (credit_in, DispatchError::Unavailable));
ensure!(
credit_in.peek() >= amount_out_min.unwrap_or(Self::Balance::zero()),
(credit_in, DispatchError::Unavailable)
);
let swap_res = SWAP.with(|b| b.borrow().get(&(path[0], path[1])).map(|v| *v));
let pool_account = match swap_res {
Some(a) => a,
None => return Err((credit_in, DispatchError::Unavailable)),
};
let credit_out = match Fungibles::withdraw(
path[1],
&pool_account,
credit_in.peek(),
Exact,
Preserve,
Polite,
) {
Ok(c) => c,
Err(_) => return Err((credit_in, DispatchError::Unavailable)),
};
let _ = Fungibles::resolve(&pool_account, credit_in)
.map_err(|c| (c, DispatchError::Unavailable))?;
Ok(credit_out)
}
fn swap_tokens_for_exact_tokens(
path: Vec<Self::AssetKind>,
credit_in: Self::Credit,
amount_out: Self::Balance,
) -> Result<(Self::Credit, Self::Credit), (Self::Credit, DispatchError)> {
ensure!(2 == path.len(), (credit_in, DispatchError::Unavailable));
ensure!(credit_in.peek() >= amount_out, (credit_in, DispatchError::Unavailable));
let swap_res = SWAP.with(|b| b.borrow().get(&(path[0], path[1])).map(|v| *v));
let pool_account = match swap_res {
Some(a) => a,
None => return Err((credit_in, DispatchError::Unavailable)),
};
let credit_out = match Fungibles::withdraw(
path[1],
&pool_account,
amount_out,
Exact,
Preserve,
Polite,
) {
Ok(c) => c,
Err(_) => return Err((credit_in, DispatchError::Unavailable)),
};
let (credit_in, change) = credit_in.split(amount_out);
let _ = Fungibles::resolve(&pool_account, credit_in)
.map_err(|c| (c, DispatchError::Unavailable))?;
Ok((credit_out, change))
}
}
pub fn pool_account(asset1: AssetId, asset2: AssetId) -> AccountId {
(1000 + asset1 * 10 + asset2 * 100).into()
}
pub fn setup_pool(asset1: AssetId, liquidity1: Balance, asset2: AssetId, liquidity2: Balance) {
let account = pool_account(asset1, asset2);
SWAP.with(|b| b.borrow_mut().insert((asset1, asset2), account));
let debt1 = Fungibles::deposit(asset1, &account, liquidity1, Exact);
let debt2 = Fungibles::deposit(asset2, &account, liquidity2, Exact);
drop(debt1);
drop(debt2);
}
pub struct WeightToFee;
impl WeightToFeeT for WeightToFee {
type Balance = Balance;
fn weight_to_fee(weight: &Weight) -> Self::Balance {
(weight.ref_time() + weight.proof_size()).into()
}
}
pub struct Fungibles {}
impl Inspect<AccountId> for Fungibles {
type AssetId = AssetId;
type Balance = Balance;
fn total_issuance(asset: Self::AssetId) -> Self::Balance {
TOTAL_ISSUANCE.with(|b| b.borrow().get(&asset).map_or(Self::Balance::zero(), |b| *b))
}
fn minimum_balance(_: Self::AssetId) -> Self::Balance {
Self::Balance::one()
}
fn total_balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance {
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
}
fn balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance {
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
}
fn reducible_balance(
asset: Self::AssetId,
who: &AccountId,
_: Preservation,
_: Fortitude,
) -> Self::Balance {
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
}
fn can_deposit(
_: Self::AssetId,
_: &AccountId,
_: Self::Balance,
_: Provenance,
) -> DepositConsequence {
unimplemented!()
}
fn can_withdraw(
_: Self::AssetId,
_: &AccountId,
_: Self::Balance,
) -> WithdrawConsequence<Self::Balance> {
unimplemented!()
}
fn asset_exists(_: Self::AssetId) -> bool {
unimplemented!()
}
}
impl Unbalanced<AccountId> for Fungibles {
fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) {
TOTAL_ISSUANCE.with(|b| b.borrow_mut().insert(asset, amount));
}
fn handle_dust(_: Dust<AccountId, Self>) {
unimplemented!()
}
fn write_balance(
asset: Self::AssetId,
who: &AccountId,
amount: Self::Balance,
) -> Result<Option<Self::Balance>, DispatchError> {
let _ = ACCOUNT.with(|b| b.borrow_mut().insert((asset, *who), amount));
Ok(None)
}
}
impl Balanced<AccountId> for Fungibles {
type OnDropCredit = DecreaseIssuance<AccountId, Self>;
type OnDropDebt = IncreaseIssuance<AccountId, Self>;
}
pub struct FungiblesMatcher;
impl MatchesFungibles<AssetId, Balance> for FungiblesMatcher {
fn matches_fungibles(
a: &MultiAsset,
) -> core::result::Result<(AssetId, Balance), xcm_executor::traits::Error> {
match a {
MultiAsset {
fun: Fungible(amount),
id:
Concrete(MultiLocation { parents: 0, interior: X1(Junction::GeneralIndex(id)) }),
} => Ok(((*id).try_into().unwrap(), *amount)),
_ => Err(xcm_executor::traits::Error::AssetNotHandled),
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
title: "XCM WeightTrader: Swap Fee Asset for Native Asset"
doc:
- audience: Runtime Dev
description: |
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 isnt an account to which an asset of some class can be deposited with a guarantee to meet the existential deposit requirement.
crates:
- name: cumulus-primitives-utility