Following the enablement of arbitrary XCM execution on Kusama and Polkadot system chains, users/wallets/dapps can interact uniformly across multiple chains directly using XCM programs, without having to figure out chain-specific pallets or calls/extrinsics.
Improving Asset Transfer UX/DX
Cross-chain asset transfers are currently hard to generalize across the ecosystem. Each Parachain uses its own assets transfer pallet (usually xtokens
or pallet-xcm
) that exposes asset transfer extrinsics unique to that chain, and wallets/dapps need to integrate with all of them. These extrinsics then build opinionated XCM programs to do the asset transfers.
With the ability to execute arbitrary XCM programs, we can make use of this powerful layer of abstraction, and slowly transition to switching that around:
Wallets/dApps define their desired asset transfers in an XCM program and execute that using the “universal” pallet-xcm::execute()
extrinsic. This has two major advantages:
- Arbitrary XCM program (vs extrinsic) allows very high flexibility in what it can do. Current
xtokens
andpallet-xcm
transfer extrinsics are limited to basic use-cases, and trying to expose more flexible ones results in ugly calls like pallet-xcm::transfer_assets_using_type_and_then(). - Allows for high reusability across multiple chains. The same XCM program (save some relative location parameters) can be reused on other chains for the same purpose.
The disadvantage of running “raw” XCM programs is that they’re hard to build, it’s a low-level language with gotchas and corner-cases, and easy to get wrong. Calling an extrinsic to build and execute an XCM program for you is definitely simpler and more reliable.
But what if we could fix (or mitigate) that?
XCM Asset Transfer Tooling
For automated, safe, reliable building of a complex asset transfer XCM we will require multiple layers of “helper” tools/libraries each operating at a different level. A quick exercise of imagination produces the following top-down view:
-
wallet/dApp: handles the “business usecase”: gets the user input and models “actions” based on it: some of these actions will be generic asset transfers like:
- “(somehow) move
AssetX
fromAccountA
onChainA
toAccountB
onChainB
”.
- “(somehow) move
-
some asset-transfer-library (ecosystem-stateful): has ecosystem-level knowledge/state, able to identify what cross-chain interactions are required to “move
AssetX
fromChainA
toChainB
”, depending on the actual assets and chains involved it can define things like:- reserve location of
AssetX
, - cross-chain path from
ChainA
toChainB
(is it direct, through a reserve chain, over a bridge, etc?), and the “hops” involved - type of transfer of each asset between each hop (teleport, reserve-deposit, reserve-withdraw)
libraries like asset-transfer-api and its assets-registry (cc @IkerParity)
- reserve location of
-
some XCM builder tool (ecosystem-stateless): Easy to use helper tool/library to hide away as much XCM complexity and be able to take the “detailed definition” of an asset transfer (involved accounts, assets, chains, transfer-types) and build a sanity-checked, corner-case-proofed, reliable XCM program for it.
This ^, alongside tooling that exposes the DryRun runtime APIs, should allow dApps to confidently switch to using flexible, portable, interoperable XCM programs instead of opinionated, chain-specific asset transfer extrinsics.
XCM Asset Transfer Builder
This RFC proposes the following (Rust) builder pattern tool for addressing layer (3) above. Same or similar can be created in JS or other relevant languages.
Builder properties:
- sanity-checks any new “actions” (against the program built so far) handling gotchas and corner-cases,
- “tag” assets in a given context (one chain) and use same tag to reference same asset in all other contexts (other chains) without having to explicitly manage XCM
Location
s context-relative views (no need to worry about reanchoring all the time), - hides away implementation details like how fees and assets might need separate transfers,
- allows “natural”, in-order sequenced definition of actions across chains (vs the XCM program which will be a Matryoska doll that is built “backwards”, from the destination to the source),
- enforces generated XCM correctness (assuming input/scenario-definition correctness).
Example 1 - HDX, USDT, GLMR from Hydration Network to Moonbeam (through AH)
fn example_hydra_to_ah_to_moonbeam() {
// Initial context needs to start with `GlobalConsensus`
let hydra_context = InteriorLocation::X2(GlobalConsensus(Polkadot), Parachain(HYDRA_PARAID));
// used assets IDs as seen by the `hydra_context` chain
let hdx_id = AssetId(Here.into());
let usdt_id = AssetId(Location::new(
1,
X3(Parachain(ASSET_HUB_PARAID), PalletInstance(50), GeneralIndex(1984)),
));
let glmr_id = AssetId(Location::new(1, X1(Parachain(MOONBEAM_PARAID))));
// Initialize the builder by defining the starting context and,
let asset_transfer_builder = AssetTransferBuilder::using_context(hydra_context)
// registering easy-to-use tags for the assets to transfer - caller can use these tags to
// easily identify the assets without having to worry about reanchoring and contexts
.define_asset("HDX", hdx_id)
.define_asset("USDT", usdt_id)
.define_asset("GLMR", glmr_id)
// create the builder
.create();
let xcm = asset_transfer_builder
// the starting context is Hydration Network
// withdraw assets to transfer from origin account
.withdraw("HDX", Definite(hdx_amount))
.withdraw("USDT", Definite(usdt_amount))
.withdraw("GLMR", Definite(glmr_amount))
// set AssetHub as the destination for this leg of the transfer
.set_next_hop(Location::new(1, X1(Parachain(ASSET_HUB_PARAID))))
// teleport all HDX to Asset Hub
.transfer("HDX", All, Teleport)
// reserve-withdraw all USDT on Asset Hub
.transfer("USDT", All, ReserveWithdraw)
// reserve-withdraw all GLMR on Asset Hub
.transfer("GLMR", All, ReserveWithdraw)
// use USDT to pay for fees on Asset Hub (can define upper limit)
.pay_remote_fess_with("USDT", Definite(max_usdt_to_use_for_fees))
// "execute" current leg of the transfer, move to next hop (Asset Hub)
.execute_hop()
// from here on, context is Asset Hub
// set Moonbeam as the destination for this (final) leg of the transfer
.set_next_hop(Location::new(1, X1(Parachain(MOONBEAM_PARAID))))
// reserve-deposit HDX to Moonbeam (note we don't need to worry about reanchoring in the new
// context)
.transfer("HDX", All, ReserveDeposit)
// reserve-deposit USDT to Moonbeam (asset reanchoring done behind the scenes)
.transfer("USDT", All, ReserveDeposit)
// teleport GLMR to Moonbeam (asset reanchoring done behind the scenes)
.transfer("GLMR", All, Teleport)
// use GLMR to pay for fees on Moonbeam (no limit)
.pay_remote_fess_with("GLMR", All)
// "execute" current leg of the transfer, move to next hop (Moonbeam)
.execute_hop()
// from here on, context is Moonbeam
// deposit all received assets to `beneficiary`
.deposit_all(beneficiary)
// build the asset transfer XCM Program!
.finalize();
// Profit!
println!("Asset transfer XCM: {:?}", xcm);
}
Example 2 - KSM from Karura to Acala (over bridge, going through both Asset Hubs)
fn example_karura_to_acala() {
// Initial context needs to start with `GlobalConsensus`
let karura_context = InteriorLocation::X2(GlobalConsensus(Kusama), Parachain(KARURA_PARAID));
// used assets IDs as seen by the `karura_context` chain
let ksm_id = AssetId(Location::new(1, Here));
// Initialize the builder by defining the starting context and,
let asset_transfer_builder = AssetTransferBuilder::using_context(karura_context)
// registering easy-to-use tags for the assets to transfer - caller can use these tags to
// easily identify the assets without having to worry about reanchoring and contexts
.define_asset("KSM", ksm_id)
// create the builder
.create();
let xcm = asset_transfer_builder
// the starting context is Karura
// withdraw assets to transfer from origin account
.withdraw("KSM", Definite(ksm_amount))
// set Kusama AssetHub as the destination for this leg of the transfer
.set_next_hop(Location::new(1, X1(Parachain(KUSAMA_ASSET_HUB_PARAID))))
// reserve-withdraw all KSM on Asset Hub
.transfer("KSM", All, ReserveWithdraw)
// use KSM to pay for fees on Kusama Asset Hub (no limit)
.pay_remote_fess_with("KSM", All)
// "execute" current leg of the transfer, move to next hop (Kusama Asset Hub)
.execute_hop()
// from here on, context is Kusama Asset Hub
// set Polkadot Asset Hub as the destination for this leg of the transfer
.set_next_hop(Location::new(
2,
X2(GlobalConsensus(Polkadot), Parachain(POLKADOT_ASSET_HUB_PARAID)),
))
// reserve-deposit KSM to Polkadot Asset Hub (asset reanchoring done behind the scenes)
.transfer("KSM", All, ReserveDeposit)
// use KSM to pay for fees on Polkadot Asset Hub (no limit)
.pay_remote_fess_with("KSM", All)
// "execute" current leg of the transfer, move to next hop (Polkadot Asset Hub)
.execute_hop()
// from here on, context is Polkadot Asset Hub
// set Acala as the destination for this leg of the transfer
.set_next_hop(Location::new(1, X1(Parachain(ACALA_PARAID))))
// reserve-deposit KSM to Acala (asset reanchoring done behind the scenes)
.transfer("KSM", All, ReserveDeposit)
// use KSM to pay for fees on Acala (no limit)
.pay_remote_fess_with("KSM", All)
// "execute" current leg of the transfer, move to next hop (Acala)
.execute_hop()
// from here on, context is Acala
// deposit all received assets to `beneficiary`
.deposit_all(beneficiary)
// build the asset transfer XCM Program!
.finalize();
// Profit!
println!("Asset transfer XCM: {:?}", xcm);
}
Github issue/discussion: RFC: XCM Asset Transfer Program Builder · Issue #4736 · paritytech/polkadot-sdk · GitHub