Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PoolAssetsExchanger to asset hubs #539

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- Asset Hubs: added an AssetExchanger to be able to swap tokens using the xcm executor, even for delivery fees ([polkadot-fellows/runtimes#539](https://github.com/polkadot-fellows/runtimes/pull/539)).
- Location conversion tests for relays and parachains ([polkadot-fellows/runtimes#487](https://github.com/polkadot-fellows/runtimes/pull/487))
- Asset Hubs: XcmPaymentApi now returns all assets in a pool with the native token as acceptable as fee payment ([polkadot-fellows/runtimes#523](https://github.com/polkadot-fellows/runtimes/pull/523))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@

use super::reserve_transfer::*;
use crate::{
foreign_balance_on,
tests::teleport::do_bidirectional_teleport_foreign_assets_between_para_and_asset_hub_using_xt,
*,
};
use asset_hub_kusama_runtime::xcm_config::KsmLocation;
use emulated_integration_tests_common::USDT_ID;

fn para_to_para_assethub_hop_assertions(t: ParaToParaThroughAHTest) {
type RuntimeEvent = <AssetHubKusama as Chain>::RuntimeEvent;
Expand Down Expand Up @@ -788,3 +790,180 @@ fn transfer_native_asset_from_relay_to_para_through_asset_hub() {
// should be non-zero
assert!(receiver_assets_after < receiver_assets_before + amount_to_send);
}

// We transfer USDT from PenpalA to PenpalB through Asset Hub.
// The sender on PenpalA pays delivery fees in KSM.
// When the message arrives to Asset Hub, execution and delivery fees are paid in USDT
// swapping for KSM automatically.
// When it arrives to PenpalB, execution fees are paid with USDT by swapping for KSM.
#[test]
fn usdt_only_transfer_from_para_to_para_through_asset_hub() {
// Initialize necessary variables.
let amount_to_send = 1_000_000_000_000;
let sender = PenpalASender::get();
let destination = PenpalA::sibling_location_of(PenpalB::para_id());
let penpal_a_as_seen_by_ah = AssetHubKusama::sibling_location_of(PenpalA::para_id());
let sov_penpal_on_ah = AssetHubKusama::sovereign_account_id_of(penpal_a_as_seen_by_ah);
let receiver = PenpalBReceiver::get();
let fee_asset_item = 0;
let usdt_location: Location =
(Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into();
let usdt_location_ah: Location = (PalletInstance(50), GeneralIndex(1984)).into();
let ksm_location = Location::parent();
let assets: Vec<Asset> = vec![(usdt_location.clone(), amount_to_send).into()];

// Sender needs some ksm to pay for delivery fees.
PenpalA::mint_foreign_asset(
<PenpalA as Chain>::RuntimeOrigin::signed(PenpalAssetOwner::get()),
ksm_location.clone(),
sender.clone(),
10_000_000_000_000,
);

// The sovereign account of PenpalA in AssetHubKusama needs to have the same amount of USDT
// since it's the reserve.
AssetHubKusama::mint_asset(
<AssetHubKusama as Chain>::RuntimeOrigin::signed(AssetHubKusamaAssetOwner::get()),
USDT_ID,
sov_penpal_on_ah,
10_000_000_000_000,
);

// Mint USDT to sender to be able to transfer.
PenpalA::mint_foreign_asset(
<PenpalA as Chain>::RuntimeOrigin::signed(PenpalAssetOwner::get()),
usdt_location.clone(),
sender.clone(),
10_000_000_000_000,
);

// AssetHubKusama has a pool between USDT and ksm so fees can be paid with USDT by automatically
// swapping them for ksm.
create_pool_with_ksm_on!(
AssetHubKusama,
usdt_location_ah,
false,
AssetHubKusamaAssetOwner::get()
);

// PenpalB has a pool between USDT and ksm so fees can be paid with USDT by automatically
// swapping them for ksm.
create_pool_with_ksm_on!(PenpalB, usdt_location.clone(), true, PenpalAssetOwner::get());

// Sender starts with a lot of USDT.
let sender_balance_before = foreign_balance_on!(PenpalA, usdt_location.clone(), &sender);
assert_eq!(sender_balance_before, 10_000_000_000_000);

// Receiver has no USDT.
let receiver_balance_before = foreign_balance_on!(PenpalB, usdt_location.clone(), &receiver);
assert_eq!(receiver_balance_before, 0);

let test_args = TestContext {
sender: sender.clone(),
receiver: receiver.clone(),
args: TestArgs::new_para(
destination.clone(),
receiver.clone(),
amount_to_send,
assets.into(),
None,
fee_asset_item,
),
};
let mut test = ParaToParaThroughAHTest::new(test_args);

// Assertions executed on the sender, PenpalA.
fn sender_assertions(_: ParaToParaThroughAHTest) {
type Event = <PenpalA as Chain>::RuntimeEvent;

let transfer_amount = 1_000_000_000_000;
let usdt_location: Location =
(Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into();

assert_expected_events!(
PenpalA,
vec![
Event::ForeignAssets(
pallet_assets::Event::Burned { asset_id, balance, .. }
) => {
asset_id: *asset_id == usdt_location.clone(),
balance: *balance == transfer_amount,
},
]
);
}

// Assertions executed on the intermediate hop, AssetHubKusama.
fn ah_assertions(_: ParaToParaThroughAHTest) {
type Event = <AssetHubKusama as Chain>::RuntimeEvent;

let transfer_amount = 1_000_000_000_000;
let penpal_a_as_seen_by_ah = AssetHubKusama::sibling_location_of(PenpalA::para_id());
let sov_penpal_on_ah = AssetHubKusama::sovereign_account_id_of(penpal_a_as_seen_by_ah);

assert_expected_events!(
AssetHubKusama,
vec![
// USDT is burned from sovereign account of PenpalA.
Event::Assets(
pallet_assets::Event::Burned { asset_id, owner, balance }
) => {
asset_id: *asset_id == 1984,
owner: *owner == sov_penpal_on_ah,
balance: *balance == transfer_amount,
},
// Credit is swapped.
Event::AssetConversion(
pallet_asset_conversion::Event::SwapCreditExecuted { .. }
) => {},
// Message from PenpalA was processed.
Event::MessageQueue(
pallet_message_queue::Event::Processed { success: true, .. }
) => {},
]
);
}

// Assertions executed on the receiver, PenpalB.
fn receiver_assertions(_: ParaToParaThroughAHTest) {
type Event = <PenpalB as Chain>::RuntimeEvent;

let usdt_location: Location =
(Parent, Parachain(1000), PalletInstance(50), GeneralIndex(1984)).into();
let receiver = PenpalBReceiver::get();
let final_amount = 990_665_188_940;

assert_expected_events!(
PenpalB,
vec![
// Final amount gets deposited to receiver.
Event::ForeignAssets(
pallet_assets::Event::Issued { asset_id, owner, amount }
) => {
asset_id: *asset_id == usdt_location,
owner: *owner == receiver,
amount: *amount == final_amount,
},
// Swap was made to pay fees with USDT.
Event::AssetConversion(
pallet_asset_conversion::Event::SwapCreditExecuted { .. }
) => {},
]
);
}

// Run test and assert.
test.set_assertion::<PenpalA>(sender_assertions);
test.set_assertion::<AssetHubKusama>(ah_assertions);
test.set_assertion::<PenpalB>(receiver_assertions);
test.set_dispatchable::<PenpalA>(para_to_para_transfer_assets_through_ah);
test.assert();

// Sender has less USDT after the transfer.
let sender_balance_after = foreign_balance_on!(PenpalA, usdt_location.clone(), &sender);
assert_eq!(sender_balance_after, 9_000_000_000_000);

// Receiver gets `transfer_amount` minus fees.
let receiver_balance_after = foreign_balance_on!(PenpalB, usdt_location.clone(), &receiver);
assert_eq!(receiver_balance_after, 990_665_188_940);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,79 @@ mod swap;
mod teleport;
mod treasury;
mod xcm_fee_estimation;

#[macro_export]
macro_rules! foreign_balance_on {
( $chain:ident, $id:expr, $who:expr ) => {
emulated_integration_tests_common::impls::paste::paste! {
<$chain>::execute_with(|| {
type ForeignAssets = <$chain as [<$chain Pallet>]>::ForeignAssets;
<ForeignAssets as Inspect<_>>::balance($id, $who)
})
}
};
}

#[macro_export]
macro_rules! create_pool_with_ksm_on {
( $chain:ident, $asset_id:expr, $is_foreign:expr, $asset_owner:expr ) => {
emulated_integration_tests_common::impls::paste::paste! {
<$chain>::execute_with(|| {
type RuntimeEvent = <$chain as Chain>::RuntimeEvent;
let owner = $asset_owner;
let signed_owner = <$chain as Chain>::RuntimeOrigin::signed(owner.clone());
let ksm_location: Location = Parent.into();
if $is_foreign {
assert_ok!(<$chain as [<$chain Pallet>]>::ForeignAssets::mint(
signed_owner.clone(),
$asset_id.clone().into(),
owner.clone().into(),
10_000_000_000_000, // For it to have more than enough.
));
} else {
let asset_id = match $asset_id.interior.last() {
Some(GeneralIndex(id)) => *id as u32,
_ => unreachable!(),
};
assert_ok!(<$chain as [<$chain Pallet>]>::Assets::mint(
signed_owner.clone(),
asset_id.into(),
owner.clone().into(),
10_000_000_000_000, // For it to have more than enough.
));
}

assert_ok!(<$chain as [<$chain Pallet>]>::AssetConversion::create_pool(
signed_owner.clone(),
Box::new(ksm_location.clone()),
Box::new($asset_id.clone()),
));

assert_expected_events!(
$chain,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {},
]
);

assert_ok!(<$chain as [<$chain Pallet>]>::AssetConversion::add_liquidity(
signed_owner,
Box::new(ksm_location),
Box::new($asset_id),
1_000_000_000_000,
2_000_000_000_000, // $asset_id is worth half of ksm
0,
0,
owner.into()
));

assert_expected_events!(
$chain,
vec![
RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {},
]
);
});
}
};
}
Loading
Loading