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-support",
"frame-system", "frame-system",
"hex-literal", "hex-literal",
"pallet-asset-conversion",
"pallet-assets", "pallet-assets",
"pallet-balances", "pallet-balances",
"pallet-collator-selection", "pallet-collator-selection",
@@ -1973,6 +1974,7 @@ dependencies = [
"frame-support", "frame-support",
"hex", "hex",
"hex-literal", "hex-literal",
"pallet-asset-conversion",
"pallet-assets", "pallet-assets",
"pallet-balances", "pallet-balances",
"pallet-bridge-messages", "pallet-bridge-messages",
@@ -2165,6 +2167,7 @@ dependencies = [
"cumulus-pallet-xcmp-queue", "cumulus-pallet-xcmp-queue",
"emulated-integration-tests-common", "emulated-integration-tests-common",
"frame-support", "frame-support",
"pallet-asset-conversion",
"pallet-assets", "pallet-assets",
"pallet-balances", "pallet-balances",
"pallet-bridge-messages", "pallet-bridge-messages",
@@ -4095,6 +4098,7 @@ dependencies = [
"cumulus-primitives-core", "cumulus-primitives-core",
"frame-support", "frame-support",
"log", "log",
"pallet-asset-conversion",
"pallet-xcm-benchmarks", "pallet-xcm-benchmarks",
"parity-scale-codec", "parity-scale-codec",
"polkadot-runtime-common", "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)), 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)), 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 } sp-core = { path = "../../../../../../../substrate/primitives/core", default-features = false }
frame-support = { path = "../../../../../../../substrate/frame/support", default-features = false } frame-support = { path = "../../../../../../../substrate/frame/support", default-features = false }
pallet-assets = { path = "../../../../../../../substrate/frame/assets", 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-balances = { path = "../../../../../../../substrate/frame/balances", default-features = false }
pallet-message-queue = { path = "../../../../../../../substrate/frame/message-queue" } pallet-message-queue = { path = "../../../../../../../substrate/frame/message-queue" }
sp-runtime = { path = "../../../../../../../substrate/primitives/runtime", default-features = false } 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}, rococo_emulated_chain::{genesis::ED as ROCOCO_ED, RococoRelayPallet as RococoPallet},
AssetHubRococoPara as AssetHubRococo, AssetHubRococoParaReceiver as AssetHubRococoReceiver, AssetHubRococoPara as AssetHubRococo, AssetHubRococoParaReceiver as AssetHubRococoReceiver,
AssetHubRococoParaSender as AssetHubRococoSender, AssetHubWestendPara as AssetHubWestend, 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, BridgeHubRococoParaSender as BridgeHubRococoSender, BridgeHubWestendPara as BridgeHubWestend,
RococoRelay as Rococo, RococoRelayReceiver as RococoReceiver, RococoRelay as Rococo, RococoRelayReceiver as RococoReceiver,
RococoRelaySender as RococoSender, RococoRelaySender as RococoSender,
@@ -49,6 +49,49 @@ fn send_rocs_from_asset_hub_rococo_to_asset_hub_westend() {
AssetHubWestend::para_id(), 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 = let rocs_in_reserve_on_ahr_before =
<AssetHubRococo as Chain>::account_data_of(sov_ahw_on_ahr.clone()).free; <AssetHubRococo as Chain>::account_data_of(sov_ahw_on_ahr.clone()).free;
let sender_rocs_before = 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()) <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); send_asset_from_asset_hub_rococo_to_asset_hub_westend(roc_at_asset_hub_rococo, amount);
AssetHubWestend::execute_with(|| { AssetHubWestend::execute_with(|| {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent; type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
@@ -16,6 +16,7 @@ codec = { package = "parity-scale-codec", version = "3.4.0", default-features =
# Substrate # Substrate
frame-support = { path = "../../../../../../../substrate/frame/support", default-features = false } frame-support = { path = "../../../../../../../substrate/frame/support", default-features = false }
pallet-assets = { path = "../../../../../../../substrate/frame/assets", 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-balances = { path = "../../../../../../../substrate/frame/balances", default-features = false }
pallet-message-queue = { path = "../../../../../../../substrate/frame/message-queue" } pallet-message-queue = { path = "../../../../../../../substrate/frame/message-queue" }
sp-runtime = { path = "../../../../../../../substrate/primitives/runtime", default-features = false } 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, westend_emulated_chain::WestendRelayPallet as WestendPallet,
AssetHubRococoPara as AssetHubRococo, AssetHubRococoParaReceiver as AssetHubRococoReceiver, 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, AssetHubWestendParaSender as AssetHubWestendSender, BridgeHubRococoPara as BridgeHubRococo,
BridgeHubWestendPara as BridgeHubWestend, BridgeHubWestendParaSender as BridgeHubWestendSender, BridgeHubWestendPara as BridgeHubWestend, BridgeHubWestendParaSender as BridgeHubWestendSender,
WestendRelay as Westend, WestendRelay as Westend,
@@ -48,6 +48,49 @@ fn send_wnds_from_asset_hub_westend_to_asset_hub_rococo() {
AssetHubRococo::para_id(), 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 = let wnds_in_reserve_on_ahw_before =
<AssetHubWestend as Chain>::account_data_of(sov_ahr_on_ahw.clone()).free; <AssetHubWestend as Chain>::account_data_of(sov_ahr_on_ahw.clone()).free;
let sender_wnds_before = let sender_wnds_before =
@@ -312,7 +312,7 @@ impl pallet_assets::Config<PoolAssetsInstance> for Runtime {
type BenchmarkHelper = (); type BenchmarkHelper = ();
} }
/// Union fungibles implementation for `Assets`` and `ForeignAssets`. /// Union fungibles implementation for `Assets` and `ForeignAssets`.
pub type LocalAndForeignAssets = fungibles::UnionOf< pub type LocalAndForeignAssets = fungibles::UnionOf<
Assets, Assets,
ForeignAssets, ForeignAssets,
@@ -324,18 +324,21 @@ pub type LocalAndForeignAssets = fungibles::UnionOf<
AccountId, 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 { impl pallet_asset_conversion::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type Balance = Balance; type Balance = Balance;
type HigherPrecisionBalance = sp_core::U256; type HigherPrecisionBalance = sp_core::U256;
type AssetKind = MultiLocation; type AssetKind = MultiLocation;
type Assets = fungible::UnionOf< type Assets = NativeAndAssets;
Balances,
LocalAndForeignAssets,
TargetFromLeft<TokenLocation>,
Self::AssetKind,
Self::AccountId,
>;
type PoolId = (Self::AssetKind, Self::AssetKind); type PoolId = (Self::AssetKind, Self::AssetKind);
type PoolLocator = type PoolLocator =
pallet_asset_conversion::WithFirstAsset<TokenLocation, AccountId, Self::AssetKind>; pallet_asset_conversion::WithFirstAsset<TokenLocation, AccountId, Self::AssetKind>;
@@ -15,17 +15,21 @@
use super::{ use super::{
AccountId, AllPalletsWithSystem, Assets, Authorship, Balance, Balances, BaseDeliveryFee, AccountId, AllPalletsWithSystem, Assets, Authorship, Balance, Balances, BaseDeliveryFee,
FeeAssetId, ForeignAssets, ForeignAssetsInstance, ParachainInfo, ParachainSystem, PolkadotXcm, CollatorSelection, FeeAssetId, ForeignAssets, ForeignAssetsInstance, ParachainInfo,
PoolAssets, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, ToWestendXcmRouter, ParachainSystem, PolkadotXcm, PoolAssets, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin,
TransactionByteFee, TrustBackedAssetsInstance, WeightToFee, XcmpQueue, ToWestendXcmRouter, TransactionByteFee, TrustBackedAssetsInstance, WeightToFee, XcmpQueue,
}; };
use assets_common::{ use assets_common::{
local_and_foreign_assets::MatchesLocalAndForeignAssetsMultiLocation, local_and_foreign_assets::MatchesLocalAndForeignAssetsMultiLocation,
matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset}, matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset},
TrustBackedAssetsAsMultiLocation,
}; };
use frame_support::{ use frame_support::{
match_types, parameter_types, 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 frame_system::EnsureRoot;
use pallet_xcm::XcmPassthrough; use pallet_xcm::XcmPassthrough;
@@ -73,6 +77,7 @@ parameter_types! {
pub PoolAssetsPalletLocation: MultiLocation = pub PoolAssetsPalletLocation: MultiLocation =
PalletInstance(<PoolAssets as PalletInfoAccess>::index() as u8).into(); PalletInstance(<PoolAssets as PalletInfoAccess>::index() as u8).into();
pub CheckingAccount: AccountId = PolkadotXcm::check_account(); pub CheckingAccount: AccountId = PolkadotXcm::check_account();
pub StakingPot: AccountId = CollatorSelection::account_id();
pub const GovernanceLocation: MultiLocation = MultiLocation::parent(); pub const GovernanceLocation: MultiLocation = MultiLocation::parent();
pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating(); pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating();
pub RelayTreasuryLocation: MultiLocation = (Parent, PalletInstance(rococo_runtime_constants::TREASURY_PALLET_ID)).into(); pub RelayTreasuryLocation: MultiLocation = (Parent, PalletInstance(rococo_runtime_constants::TREASURY_PALLET_ID)).into();
@@ -550,31 +555,17 @@ impl xcm_executor::Config for XcmConfig {
>; >;
type Trader = ( type Trader = (
UsingComponents<WeightToFee, TokenLocation, AccountId, Balances, ToStakingPot<Runtime>>, UsingComponents<WeightToFee, TokenLocation, AccountId, Balances, ToStakingPot<Runtime>>,
// This trader allows to pay with `is_sufficient=true` "Trust Backed" assets from dedicated cumulus_primitives_utility::SwapFirstAssetTrader<
// `pallet_assets` instance - `Assets`. TokenLocation,
cumulus_primitives_utility::TakeFirstAssetTrader< crate::AssetConversion,
WeightToFee,
crate::NativeAndAssets,
(
TrustBackedAssetsAsMultiLocation<TrustBackedAssetsPalletLocation, Balance>,
ForeignAssetsConvertedConcreteId,
),
ResolveAssetTo<StakingPot, crate::NativeAndAssets>,
AccountId, AccountId,
AssetFeeAsExistentialDepositMultiplierFeeCharger,
TrustBackedAssetsConvertedConcreteId,
Assets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
FungiblesTransactor,
AccountId,
XcmAssetFeesReceiver,
>,
>,
// This trader allows to pay with `is_sufficient=true` "Foreign" assets from dedicated
// `pallet_assets` instance - `ForeignAssets`.
cumulus_primitives_utility::TakeFirstAssetTrader<
AccountId,
ForeignAssetFeeAsExistentialDepositMultiplierFeeCharger,
ForeignAssetsConvertedConcreteId,
ForeignAssets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
ForeignFungiblesTransactor,
AccountId,
XcmAssetFeesReceiver,
>,
>, >,
); );
type ResponseHandler = PolkadotXcm; type ResponseHandler = PolkadotXcm;
@@ -17,34 +17,38 @@
//! Tests for the Rococo Assets Hub chain. //! Tests for the Rococo Assets Hub chain.
use asset_hub_rococo_runtime::xcm_config::{ use asset_hub_rococo_runtime::{
AssetFeeAsExistentialDepositMultiplierFeeCharger, TokenLocation, xcm_config,
TrustBackedAssetsPalletLocation, xcm_config::{bridging, ForeignCreatorsSovereignAccountOf, LocationToAccountId, TokenLocation},
AllPalletsWithoutSystem, MetadataDepositBase, MetadataDepositPerByte, RuntimeCall,
RuntimeEvent, ToWestendXcmRouterInstance, XcmpQueue,
}; };
pub use asset_hub_rococo_runtime::{ pub use asset_hub_rococo_runtime::{
xcm_config::{ xcm_config::{CheckingAccount, TrustBackedAssetsPalletLocation, XcmConfig},
self, bridging, CheckingAccount, ForeignCreatorsSovereignAccountOf, LocationToAccountId, AssetConversion, AssetDeposit, Assets, Balances, CollatorSelection, ExistentialDeposit,
XcmConfig, ForeignAssets, ForeignAssetsInstance, ParachainSystem, Runtime, SessionKeys, System,
}, TrustBackedAssetsInstance,
AllPalletsWithoutSystem, AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets,
ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem, Runtime,
RuntimeCall, RuntimeEvent, SessionKeys, System, ToWestendXcmRouterInstance,
TrustBackedAssetsInstance, XcmpQueue,
}; };
use asset_test_utils::{ use asset_test_utils::{
test_cases_over_bridge::TestBridgingConfig, CollatorSessionKey, CollatorSessionKeys, ExtBuilder, test_cases_over_bridge::TestBridgingConfig, CollatorSessionKey, CollatorSessionKeys, ExtBuilder,
}; };
use codec::{Decode, Encode}; use codec::{Decode, Encode};
use cumulus_primitives_utility::ChargeWeightInFungibles;
use frame_support::{ use frame_support::{
assert_noop, assert_ok, assert_ok,
traits::fungibles::InspectEnumerable, traits::{
fungible::{Inspect, Mutate},
fungibles::{
Create, Inspect as FungiblesInspect, InspectEnumerable, Mutate as FungiblesMutate,
},
},
weights::{Weight, WeightToFee as WeightToFeeT}, weights::{Weight, WeightToFee as WeightToFeeT},
}; };
use parachains_common::{ 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 sp_runtime::traits::MaybeEquivalence;
use std::convert::Into;
use xcm::latest::prelude::*; use xcm::latest::prelude::*;
use xcm_executor::traits::{Identity, JustTry, WeightTrader}; use xcm_executor::traits::{Identity, JustTry, WeightTrader};
@@ -69,7 +73,7 @@ fn collator_session_keys() -> CollatorSessionKeys<Runtime> {
} }
#[test] #[test]
fn test_asset_xcm_trader() { fn test_buy_and_refund_weight_in_native() {
ExtBuilder::<Runtime>::default() ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)]) .with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![( .with_session_keys(vec![(
@@ -79,77 +83,55 @@ fn test_asset_xcm_trader() {
)]) )])
.build() .build()
.execute_with(|| { .execute_with(|| {
// We need root origin to create a sufficient asset let bob: AccountId = SOME_ASSET_ADMIN.into();
let minimum_asset_balance = 3333333_u128; let staking_pot = CollatorSelection::account_id();
let local_asset_id = 1; let native_location = TokenLocation::get();
assert_ok!(Assets::force_create( let initial_balance = 200 * UNITS;
RuntimeHelper::root_origin(),
local_asset_id.into(),
AccountId::from(ALICE).into(),
true,
minimum_asset_balance
));
// We first mint enough asset for the account to exist for assets assert_ok!(Balances::mint_into(&bob, initial_balance));
assert_ok!(Assets::mint( assert_ok!(Balances::mint_into(&staking_pot, initial_balance));
RuntimeHelper::origin_of(AccountId::from(ALICE)),
local_asset_id.into(),
AccountId::from(ALICE).into(),
minimum_asset_balance
));
// get asset id as multilocation // keep initial total issuance to assert later.
let asset_multilocation = let total_issuance = Balances::total_issuance();
AssetIdForTrustBackedAssetsConvert::convert_back(&local_asset_id).unwrap();
// Set Alice as block author, who will receive fees // prepare input to buy weight.
RuntimeHelper::run_to_block(2, AccountId::from(ALICE)); let weight = Weight::from_parts(4_000_000_000, 0);
let fee = WeightToFee::weight_to_fee(&weight);
// We are going to buy 4e9 weight let extra_amount = 100;
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();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None }; 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 // init trader and buy weight.
let unused_assets = trader.buy_weight(bought, asset.into(), &ctx).expect("Expected Ok"); let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
// Check whether a correct amount of unused assets is returned let unused_asset =
assert_ok!( trader.buy_weight(weight, payment.into(), &ctx).expect("Expected Ok");
unused_assets.ensure_contains(&(asset_multilocation, asset_amount_extra).into())
);
// 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); drop(trader);
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
// Make sure author(Alice) has received the amount assert_eq!(Balances::total_issuance(), total_issuance + fee - refund);
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
);
});
} }
#[test] #[test]
fn test_asset_xcm_trader_with_refund() { fn test_buy_and_refund_weight_with_swap_local_asset_xcm_trader() {
ExtBuilder::<Runtime>::default() ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)]) .with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![( .with_session_keys(vec![(
@@ -159,249 +141,192 @@ fn test_asset_xcm_trader_with_refund() {
)]) )])
.build() .build()
.execute_with(|| { .execute_with(|| {
// We need root origin to create a sufficient asset let bob: AccountId = SOME_ASSET_ADMIN.into();
// We set existential deposit to be identical to the one for Balances first let staking_pot = CollatorSelection::account_id();
assert_ok!(Assets::force_create( let asset_1: u32 = 1;
RuntimeHelper::root_origin(), let native_location = TokenLocation::get();
1.into(), let asset_1_location =
AccountId::from(ALICE).into(), AssetIdForTrustBackedAssetsConvert::convert_back(&asset_1).unwrap();
true, // bob's initial balance for native and `asset1` assets.
ExistentialDeposit::get() 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!(AssetConversion::add_liquidity(
assert_ok!(Assets::mint( RuntimeHelper::origin_of(bob.clone()),
RuntimeHelper::origin_of(AccountId::from(ALICE)), Box::new(native_location),
1.into(), Box::new(asset_1_location),
AccountId::from(ALICE).into(), pool_liquidity,
ExistentialDeposit::get() 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 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 // init trader and buy weight.
RuntimeHelper::run_to_block(2, AccountId::from(ALICE)); 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 // assert.
let bought = Weight::from_parts(4_000_000_000u64, 0); 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 // refund.
let amount_bought = WeightToFee::weight_to_fee(&bought); let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
assert_eq!(actual_refund, (asset_1_location, asset_refund).into());
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));
// 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!( assert_eq!(
trader.refund_weight(bought - weight_used, &ctx), Assets::total_issuance(asset_1),
Some((asset_multilocation, amount_refunded).into()) asset_total_issuance + asset_fee - asset_refund
); );
assert_eq!(Balances::total_issuance(), native_total_issuance);
})
}
// Drop trader #[test]
drop(trader); 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 // init asset, balances and pool.
let fees_paid = WeightToFee::weight_to_fee(&weight_used); 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!( assert_eq!(
Assets::balance(1, AccountId::from(ALICE)), ForeignAssets::total_issuance(foreign_location),
ExistentialDeposit::get() + fees_paid asset_total_issuance + asset_fee
); );
// We also need to ensure the total supply increased // prepare input to refund weight.
assert_eq!(Assets::total_supply(1), ExistentialDeposit::get() + fees_paid); 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] // refund.
fn test_asset_xcm_trader_refund_not_possible_since_amount_less_than_ed() { let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
ExtBuilder::<Runtime>::default() assert_eq!(actual_refund, (foreign_location, asset_refund).into());
.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(); // assert.
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None }; assert_eq!(Balances::balance(&staking_pot), initial_balance);
// only after `trader` is dropped we expect the fee to be resolved into the treasury
// Set Alice as block author, who will receive fees // account.
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
drop(trader); drop(trader);
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
// Make sure author(Alice) has received the amount assert_eq!(
assert_eq!(Assets::balance(1, AccountId::from(ALICE)), ExistentialDeposit::get()); ForeignAssets::total_issuance(foreign_location),
asset_total_issuance + asset_fee - asset_refund
// We also need to ensure the total supply increased );
assert_eq!(Assets::total_supply(1), ExistentialDeposit::get()); assert_eq!(Balances::total_issuance(), native_total_issuance);
}); })
}
#[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);
});
} }
#[test] #[test]
@@ -295,7 +295,7 @@ impl pallet_assets::Config<PoolAssetsInstance> for Runtime {
type BenchmarkHelper = (); type BenchmarkHelper = ();
} }
/// Union fungibles implementation for `Assets`` and `ForeignAssets`. /// Union fungibles implementation for `Assets` and `ForeignAssets`.
pub type LocalAndForeignAssets = fungibles::UnionOf< pub type LocalAndForeignAssets = fungibles::UnionOf<
Assets, Assets,
ForeignAssets, ForeignAssets,
@@ -307,18 +307,21 @@ pub type LocalAndForeignAssets = fungibles::UnionOf<
AccountId, 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 { impl pallet_asset_conversion::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type Balance = Balance; type Balance = Balance;
type HigherPrecisionBalance = sp_core::U256; type HigherPrecisionBalance = sp_core::U256;
type AssetKind = MultiLocation; type AssetKind = MultiLocation;
type Assets = fungible::UnionOf< type Assets = NativeAndAssets;
Balances,
LocalAndForeignAssets,
TargetFromLeft<WestendLocation>,
Self::AssetKind,
Self::AccountId,
>;
type PoolId = (Self::AssetKind, Self::AssetKind); type PoolId = (Self::AssetKind, Self::AssetKind);
type PoolLocator = type PoolLocator =
pallet_asset_conversion::WithFirstAsset<WestendLocation, AccountId, Self::AssetKind>; pallet_asset_conversion::WithFirstAsset<WestendLocation, AccountId, Self::AssetKind>;
@@ -15,17 +15,21 @@
use super::{ use super::{
AccountId, AllPalletsWithSystem, Assets, Authorship, Balance, Balances, BaseDeliveryFee, AccountId, AllPalletsWithSystem, Assets, Authorship, Balance, Balances, BaseDeliveryFee,
FeeAssetId, ForeignAssets, ForeignAssetsInstance, ParachainInfo, ParachainSystem, PolkadotXcm, CollatorSelection, FeeAssetId, ForeignAssets, ForeignAssetsInstance, ParachainInfo,
PoolAssets, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, ToRococoXcmRouter, ParachainSystem, PolkadotXcm, PoolAssets, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin,
TransactionByteFee, TrustBackedAssetsInstance, WeightToFee, XcmpQueue, ToRococoXcmRouter, TransactionByteFee, TrustBackedAssetsInstance, WeightToFee, XcmpQueue,
}; };
use assets_common::{ use assets_common::{
local_and_foreign_assets::MatchesLocalAndForeignAssetsMultiLocation, local_and_foreign_assets::MatchesLocalAndForeignAssetsMultiLocation,
matching::{FromSiblingParachain, IsForeignConcreteAsset}, matching::{FromSiblingParachain, IsForeignConcreteAsset},
TrustBackedAssetsAsMultiLocation,
}; };
use frame_support::{ use frame_support::{
match_types, parameter_types, 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 frame_system::EnsureRoot;
use pallet_xcm::XcmPassthrough; use pallet_xcm::XcmPassthrough;
@@ -70,6 +74,7 @@ parameter_types! {
pub PoolAssetsPalletLocation: MultiLocation = pub PoolAssetsPalletLocation: MultiLocation =
PalletInstance(<PoolAssets as PalletInfoAccess>::index() as u8).into(); PalletInstance(<PoolAssets as PalletInfoAccess>::index() as u8).into();
pub CheckingAccount: AccountId = PolkadotXcm::check_account(); pub CheckingAccount: AccountId = PolkadotXcm::check_account();
pub StakingPot: AccountId = CollatorSelection::account_id();
pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating(); pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating();
pub RelayTreasuryLocation: MultiLocation = (Parent, PalletInstance(westend_runtime_constants::TREASURY_PALLET_ID)).into(); pub RelayTreasuryLocation: MultiLocation = (Parent, PalletInstance(westend_runtime_constants::TREASURY_PALLET_ID)).into();
} }
@@ -567,31 +572,17 @@ impl xcm_executor::Config for XcmConfig {
>; >;
type Trader = ( type Trader = (
UsingComponents<WeightToFee, WestendLocation, AccountId, Balances, ToStakingPot<Runtime>>, UsingComponents<WeightToFee, WestendLocation, AccountId, Balances, ToStakingPot<Runtime>>,
// This trader allows to pay with `is_sufficient=true` "Trust Backed" assets from dedicated cumulus_primitives_utility::SwapFirstAssetTrader<
// `pallet_assets` instance - `Assets`. WestendLocation,
cumulus_primitives_utility::TakeFirstAssetTrader< crate::AssetConversion,
WeightToFee,
crate::NativeAndAssets,
(
TrustBackedAssetsAsMultiLocation<TrustBackedAssetsPalletLocation, Balance>,
ForeignAssetsConvertedConcreteId,
),
ResolveAssetTo<StakingPot, crate::NativeAndAssets>,
AccountId, AccountId,
AssetFeeAsExistentialDepositMultiplierFeeCharger,
TrustBackedAssetsConvertedConcreteId,
Assets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
FungiblesTransactor,
AccountId,
XcmAssetFeesReceiver,
>,
>,
// This trader allows to pay with `is_sufficient=true` "Foreign" assets from dedicated
// `pallet_assets` instance - `ForeignAssets`.
cumulus_primitives_utility::TakeFirstAssetTrader<
AccountId,
ForeignAssetFeeAsExistentialDepositMultiplierFeeCharger,
ForeignAssetsConvertedConcreteId,
ForeignAssets,
cumulus_primitives_utility::XcmFeesTo32ByteAccount<
ForeignFungiblesTransactor,
AccountId,
XcmAssetFeesReceiver,
>,
>, >,
); );
type ResponseHandler = PolkadotXcm; type ResponseHandler = PolkadotXcm;
@@ -18,28 +18,36 @@
//! Tests for the Westmint (Westend Assets Hub) chain. //! Tests for the Westmint (Westend Assets Hub) chain.
use asset_hub_westend_runtime::{ use asset_hub_westend_runtime::{
xcm_config,
xcm_config::{ xcm_config::{
self, bridging, AssetFeeAsExistentialDepositMultiplierFeeCharger, CheckingAccount, bridging, ForeignCreatorsSovereignAccountOf, LocationToAccountId, WestendLocation,
ForeignCreatorsSovereignAccountOf, LocationToAccountId, TrustBackedAssetsPalletLocation,
WestendLocation, XcmConfig,
}, },
AllPalletsWithoutSystem, AssetDeposit, Assets, Balances, ExistentialDeposit, ForeignAssets, AllPalletsWithoutSystem, MetadataDepositBase, MetadataDepositPerByte, PolkadotXcm, RuntimeCall,
ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem, RuntimeEvent, RuntimeOrigin, ToRococoXcmRouterInstance, XcmpQueue,
PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys, };
ToRococoXcmRouterInstance, TrustBackedAssetsInstance, 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::{ use asset_test_utils::{
test_cases_over_bridge::TestBridgingConfig, CollatorSessionKey, CollatorSessionKeys, ExtBuilder, test_cases_over_bridge::TestBridgingConfig, CollatorSessionKey, CollatorSessionKeys, ExtBuilder,
}; };
use codec::{Decode, Encode}; use codec::{Decode, Encode};
use cumulus_primitives_utility::ChargeWeightInFungibles;
use frame_support::{ use frame_support::{
assert_noop, assert_ok, assert_ok,
traits::fungibles::InspectEnumerable, traits::{
fungible::{Inspect, Mutate},
fungibles::{
Create, Inspect as FungiblesInspect, InspectEnumerable, Mutate as FungiblesMutate,
},
},
weights::{Weight, WeightToFee as WeightToFeeT}, weights::{Weight, WeightToFee as WeightToFeeT},
}; };
use parachains_common::{ 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 sp_runtime::traits::MaybeEquivalence;
use std::convert::Into; use std::convert::Into;
@@ -67,7 +75,7 @@ fn collator_session_keys() -> CollatorSessionKeys<Runtime> {
} }
#[test] #[test]
fn test_asset_xcm_trader() { fn test_buy_and_refund_weight_in_native() {
ExtBuilder::<Runtime>::default() ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)]) .with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![( .with_session_keys(vec![(
@@ -77,77 +85,55 @@ fn test_asset_xcm_trader() {
)]) )])
.build() .build()
.execute_with(|| { .execute_with(|| {
// We need root origin to create a sufficient asset let bob: AccountId = SOME_ASSET_ADMIN.into();
let minimum_asset_balance = 3333333_u128; let staking_pot = CollatorSelection::account_id();
let local_asset_id = 1; let native_location = WestendLocation::get();
assert_ok!(Assets::force_create( let initial_balance = 200 * UNITS;
RuntimeHelper::root_origin(),
local_asset_id.into(),
AccountId::from(ALICE).into(),
true,
minimum_asset_balance
));
// We first mint enough asset for the account to exist for assets assert_ok!(Balances::mint_into(&bob, initial_balance));
assert_ok!(Assets::mint( assert_ok!(Balances::mint_into(&staking_pot, initial_balance));
RuntimeHelper::origin_of(AccountId::from(ALICE)),
local_asset_id.into(),
AccountId::from(ALICE).into(),
minimum_asset_balance
));
// get asset id as multilocation // keep initial total issuance to assert later.
let asset_multilocation = let total_issuance = Balances::total_issuance();
AssetIdForTrustBackedAssetsConvert::convert_back(&local_asset_id).unwrap();
// Set Alice as block author, who will receive fees // prepare input to buy weight.
RuntimeHelper::run_to_block(2, AccountId::from(ALICE)); let weight = Weight::from_parts(4_000_000_000, 0);
let fee = WeightToFee::weight_to_fee(&weight);
// We are going to buy 4e9 weight let extra_amount = 100;
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();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None }; 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 // init trader and buy weight.
let unused_assets = trader.buy_weight(bought, asset.into(), &ctx).expect("Expected Ok"); let mut trader = <XcmConfig as xcm_executor::Config>::Trader::new();
// Check whether a correct amount of unused assets is returned let unused_asset =
assert_ok!( trader.buy_weight(weight, payment.into(), &ctx).expect("Expected Ok");
unused_assets.ensure_contains(&(asset_multilocation, asset_amount_extra).into())
);
// 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); drop(trader);
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
// Make sure author(Alice) has received the amount assert_eq!(Balances::total_issuance(), total_issuance + fee - refund);
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
);
});
} }
#[test] #[test]
fn test_asset_xcm_trader_with_refund() { fn test_buy_and_refund_weight_with_swap_local_asset_xcm_trader() {
ExtBuilder::<Runtime>::default() ExtBuilder::<Runtime>::default()
.with_collators(vec![AccountId::from(ALICE)]) .with_collators(vec![AccountId::from(ALICE)])
.with_session_keys(vec![( .with_session_keys(vec![(
@@ -157,247 +143,192 @@ fn test_asset_xcm_trader_with_refund() {
)]) )])
.build() .build()
.execute_with(|| { .execute_with(|| {
// We need root origin to create a sufficient asset let bob: AccountId = SOME_ASSET_ADMIN.into();
// We set existential deposit to be identical to the one for Balances first let staking_pot = CollatorSelection::account_id();
assert_ok!(Assets::force_create( let asset_1: u32 = 1;
RuntimeHelper::root_origin(), let native_location = WestendLocation::get();
1.into(), let asset_1_location =
AccountId::from(ALICE).into(), AssetIdForTrustBackedAssetsConvert::convert_back(&asset_1).unwrap();
true, // bob's initial balance for native and `asset1` assets.
ExistentialDeposit::get() 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!(AssetConversion::add_liquidity(
assert_ok!(Assets::mint( RuntimeHelper::origin_of(bob.clone()),
RuntimeHelper::origin_of(AccountId::from(ALICE)), Box::new(native_location),
1.into(), Box::new(asset_1_location),
AccountId::from(ALICE).into(), pool_liquidity,
ExistentialDeposit::get() 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 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 // init trader and buy weight.
RuntimeHelper::run_to_block(2, AccountId::from(ALICE)); 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 // assert.
let bought = Weight::from_parts(4_000_000_000u64, 0); let unused_amount =
let asset_multilocation = AssetIdForTrustBackedAssetsConvert::convert_back(&1).unwrap(); 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 // prepare input to refund weight.
let amount_bought = WeightToFee::weight_to_fee(&bought); 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(); // refund.
let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
// Make sure buy_weight does not return an error assert_eq!(actual_refund, (asset_1_location, asset_refund).into());
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));
// 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!( assert_eq!(
trader.refund_weight(bought - weight_used, &ctx), Assets::total_issuance(asset_1),
Some((asset_multilocation, amount_refunded).into()) asset_total_issuance + asset_fee - asset_refund
); );
assert_eq!(Balances::total_issuance(), native_total_issuance);
})
}
// Drop trader #[test]
drop(trader); 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 // init asset, balances and pool.
let fees_paid = WeightToFee::weight_to_fee(&weight_used); 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!( assert_eq!(
Assets::balance(1, AccountId::from(ALICE)), ForeignAssets::total_issuance(foreign_location),
ExistentialDeposit::get() + fees_paid asset_total_issuance + asset_fee
); );
// We also need to ensure the total supply increased // prepare input to refund weight.
assert_eq!(Assets::total_supply(1), ExistentialDeposit::get() + fees_paid); 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] // refund.
fn test_asset_xcm_trader_refund_not_possible_since_amount_less_than_ed() { let actual_refund = trader.refund_weight(refund_weight, &ctx).unwrap();
ExtBuilder::<Runtime>::default() assert_eq!(actual_refund, (foreign_location, asset_refund).into());
.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(); // assert.
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None }; assert_eq!(Balances::balance(&staking_pot), initial_balance);
// only after `trader` is dropped we expect the fee to be resolved into the treasury
// Set Alice as block author, who will receive fees // account.
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
drop(trader); drop(trader);
assert_eq!(Balances::balance(&staking_pot), initial_balance + fee - refund);
// Make sure author(Alice) has received the amount assert_eq!(
assert_eq!(Assets::balance(1, AccountId::from(ALICE)), ExistentialDeposit::get()); ForeignAssets::total_issuance(foreign_location),
asset_total_issuance + asset_fee - asset_refund
// We also need to ensure the total supply increased );
assert_eq!(Assets::total_supply(1), ExistentialDeposit::get()); assert_eq!(Balances::total_issuance(), native_total_issuance);
}); })
}
#[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);
});
} }
#[test] #[test]
@@ -47,6 +47,16 @@ pub type TrustBackedAssetsConvertedConcreteId<TrustBackedAssetsPalletLocation, B
/// AssetId used for identifying assets by MultiLocation. /// AssetId used for identifying assets by MultiLocation.
pub type MultiLocationForAssetId = 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`. /// [`MatchedConvertedConcreteId`] converter dedicated for storing `AssetId` as `MultiLocation`.
pub type MultiLocationConvertedConcreteId<MultiLocationFilter, Balance> = pub type MultiLocationConvertedConcreteId<MultiLocationFilter, Balance> =
MatchedConvertedConcreteId< 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-support = { path = "../../../../../substrate/frame/support", default-features = false }
frame-system = { path = "../../../../../substrate/frame/system", default-features = false } frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
pallet-assets = { path = "../../../../../substrate/frame/assets", 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-balances = { path = "../../../../../substrate/frame/balances", default-features = false }
pallet-session = { path = "../../../../../substrate/frame/session", default-features = false } pallet-session = { path = "../../../../../substrate/frame/session", default-features = false }
sp-consensus-aura = { path = "../../../../../substrate/primitives/consensus/aura", 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", "cumulus-test-relay-sproof-builder/std",
"frame-support/std", "frame-support/std",
"frame-system/std", "frame-system/std",
"pallet-asset-conversion/std",
"pallet-assets/std", "pallet-assets/std",
"pallet-balances/std", "pallet-balances/std",
"pallet-collator-selection/std", "pallet-collator-selection/std",
@@ -30,6 +30,7 @@ use parachains_runtimes_test_utils::{
ValidatorIdOf, XcmReceivedFrom, ValidatorIdOf, XcmReceivedFrom,
}; };
use sp_runtime::{traits::StaticLookup, Saturating}; use sp_runtime::{traits::StaticLookup, Saturating};
use sp_std::ops::Mul;
use xcm::{latest::prelude::*, VersionedMultiAssets}; use xcm::{latest::prelude::*, VersionedMultiAssets};
use xcm_builder::{CreateMatcher, MatchXcm}; use xcm_builder::{CreateMatcher, MatchXcm};
use xcm_executor::{traits::ConvertLocation, XcmExecutor}; use xcm_executor::{traits::ConvertLocation, XcmExecutor};
@@ -336,12 +337,13 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
+ pallet_collator_selection::Config + pallet_collator_selection::Config
+ cumulus_pallet_parachain_system::Config + cumulus_pallet_parachain_system::Config
+ cumulus_pallet_xcmp_queue::Config + cumulus_pallet_xcmp_queue::Config
+ pallet_assets::Config<ForeignAssetsPalletInstance>, + pallet_assets::Config<ForeignAssetsPalletInstance>
+ pallet_asset_conversion::Config,
AllPalletsWithoutSystem: AllPalletsWithoutSystem:
OnInitialize<BlockNumberFor<Runtime>> + OnFinalize<BlockNumberFor<Runtime>>, OnInitialize<BlockNumberFor<Runtime>> + OnFinalize<BlockNumberFor<Runtime>>,
AccountIdOf<Runtime>: Into<[u8; 32]>, AccountIdOf<Runtime>: Into<[u8; 32]> + From<[u8; 32]>,
ValidatorIdOf<Runtime>: From<AccountIdOf<Runtime>>, ValidatorIdOf<Runtime>: From<AccountIdOf<Runtime>>,
BalanceOf<Runtime>: From<Balance>, BalanceOf<Runtime>: From<Balance> + Into<Balance>,
XcmConfig: xcm_executor::Config, XcmConfig: xcm_executor::Config,
LocationToAccountId: ConvertLocation<AccountIdOf<Runtime>>, LocationToAccountId: ConvertLocation<AccountIdOf<Runtime>>,
<Runtime as pallet_assets::Config<ForeignAssetsPalletInstance>>::AssetId: <Runtime as pallet_assets::Config<ForeignAssetsPalletInstance>>::AssetId:
@@ -354,6 +356,9 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
+ Into<AccountId>, + Into<AccountId>,
<<Runtime as frame_system::Config>::Lookup as StaticLookup>::Source: <<Runtime as frame_system::Config>::Lookup as StaticLookup>::Source:
From<<Runtime as frame_system::Config>::AccountId>, 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, ForeignAssetsPalletInstance: 'static,
{ {
ExtBuilder::<Runtime>::default() 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 // Balances before
assert_eq!( assert_eq!(
<pallet_balances::Pallet<Runtime>>::free_balance(&target_account), <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()); assert_ok!(outcome.ensure_complete());
// author actual balance after (received fees from Trader for ForeignAssets) // Balances after
let author_received_fees = // staking pot receives xcm fees in dot
<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance( assert!(
foreign_asset_id_multilocation.into(), <pallet_balances::Pallet<Runtime>>::free_balance(&staking_pot) !=
&block_author_account, existential_deposit
); );
// Balances after (untouched)
assert_eq!( assert_eq!(
<pallet_balances::Pallet<Runtime>>::free_balance(&target_account), <pallet_balances::Pallet<Runtime>>::free_balance(&target_account),
existential_deposit.clone() 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), <pallet_balances::Pallet<Runtime>>::free_balance(&block_author_account),
0.into() 0.into()
); );
assert_eq!(
<pallet_balances::Pallet<Runtime>>::free_balance(&staking_pot),
existential_deposit.clone()
);
// ForeignAssets balances after // ForeignAssets balances after
assert_eq!( assert!(
<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance( <pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
foreign_asset_id_multilocation.into(), foreign_asset_id_multilocation.into(),
&target_account &target_account
), ) > 0.into()
(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
); );
assert_eq!( assert_eq!(
<pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance( <pallet_assets::Pallet<Runtime, ForeignAssetsPalletInstance>>::balance(
@@ -528,6 +556,13 @@ pub fn receive_reserve_asset_deposited_from_different_consensus_works<
), ),
0.into() 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-io = { path = "../../../substrate/primitives/io", default-features = false }
sp-runtime = { path = "../../../substrate/primitives/runtime", default-features = false } sp-runtime = { path = "../../../substrate/primitives/runtime", default-features = false }
sp-std = { path = "../../../substrate/primitives/std", 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
polkadot-runtime-common = { path = "../../../polkadot/runtime/common", default-features = false } polkadot-runtime-common = { path = "../../../polkadot/runtime/common", default-features = false }
@@ -37,6 +38,7 @@ std = [
"cumulus-primitives-core/std", "cumulus-primitives-core/std",
"frame-support/std", "frame-support/std",
"log/std", "log/std",
"pallet-asset-conversion/std",
"pallet-xcm-benchmarks/std", "pallet-xcm-benchmarks/std",
"polkadot-runtime-common/std", "polkadot-runtime-common/std",
"polkadot-runtime-parachains/std", "polkadot-runtime-parachains/std",
@@ -51,6 +53,7 @@ std = [
runtime-benchmarks = [ runtime-benchmarks = [
"cumulus-primitives-core/runtime-benchmarks", "cumulus-primitives-core/runtime-benchmarks",
"frame-support/runtime-benchmarks", "frame-support/runtime-benchmarks",
"pallet-asset-conversion/runtime-benchmarks",
"pallet-xcm-benchmarks/runtime-benchmarks", "pallet-xcm-benchmarks/runtime-benchmarks",
"polkadot-runtime-common/runtime-benchmarks", "polkadot-runtime-common/runtime-benchmarks",
"polkadot-runtime-parachains/runtime-benchmarks", "polkadot-runtime-parachains/runtime-benchmarks",
+251 -19
View File
@@ -22,19 +22,24 @@
use codec::Encode; use codec::Encode;
use cumulus_primitives_core::{MessageSendError, UpwardMessageSender}; use cumulus_primitives_core::{MessageSendError, UpwardMessageSender};
use frame_support::{ use frame_support::{
traits::{ defensive,
tokens::{fungibles, fungibles::Inspect}, traits::{tokens::fungibles, Get, OnUnbalanced as OnUnbalancedT},
Get, weights::{Weight, WeightToFee as WeightToFeeT},
},
weights::Weight,
}; };
use pallet_asset_conversion::SwapCredit as SwapCreditT;
use polkadot_runtime_common::xcm_sender::PriceForMessageDelivery; 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 sp_std::{marker::PhantomData, prelude::*};
use xcm::{latest::prelude::*, WrapVersion}; use xcm::{latest::prelude::*, WrapVersion};
use xcm_builder::TakeRevenue; use xcm_builder::TakeRevenue;
use xcm_executor::traits::{MatchesFungibles, TransactAsset, WeightTrader}; 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 /// 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 /// the given UMP `UpwardMessageSender` implementation. Thus this essentially adapts an
/// `UpwardMessageSender` trait impl into a `SendXcm` trait impl. /// `UpwardMessageSender` trait impl into a `SendXcm` trait impl.
@@ -286,23 +291,238 @@ impl<
/// in such assetId for that amount of weight /// in such assetId for that amount of weight
pub trait ChargeWeightInFungibles<AccountId, Assets: fungibles::Inspect<AccountId>> { pub trait ChargeWeightInFungibles<AccountId, Assets: fungibles::Inspect<AccountId>> {
fn charge_weight_in_fungibles( fn charge_weight_in_fungibles(
asset_id: <Assets as Inspect<AccountId>>::AssetId, asset_id: <Assets as fungibles::Inspect<AccountId>>::AssetId,
weight: Weight, 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)] #[cfg(test)]
mod tests { mod test_xcm_router {
use super::*; use super::*;
use cumulus_primitives_core::UpwardMessage; 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) /// Validates [`validate`] for required Some(destination) and Some(message)
struct OkFixedXcmHashWithAssertingRequiredInputsSender; struct OkFixedXcmHashWithAssertingRequiredInputsSender;
@@ -398,6 +618,18 @@ mod tests {
)>(dest.into(), message) )>(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] #[test]
fn take_first_asset_trader_buy_weight_called_twice_throws_error() { fn take_first_asset_trader_buy_weight_called_twice_throws_error() {
@@ -491,9 +723,9 @@ mod tests {
struct FeeChargerAssetsHandleRefund; struct FeeChargerAssetsHandleRefund;
impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund { impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund {
fn charge_weight_in_fungibles( fn charge_weight_in_fungibles(
_: <TestAssets as Inspect<TestAccountId>>::AssetId, _: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
_: Weight, _: Weight,
) -> Result<<TestAssets as Inspect<TestAccountId>>::Balance, XcmError> { ) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
Ok(AMOUNT) 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