paritytech / polkadot-sdk

The Parity Polkadot Blockchain SDK
https://polkadot.network/
1.62k stars 565 forks source link

RFC: XCM Asset Transfer Program Builder #4736

Open acatangiu opened 3 weeks ago

acatangiu commented 3 weeks ago

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:

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:

  1. 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 from AccountA on ChainA to AccountB on ChainB".
  2. some asset-transfer-library (ecosystem-stateful): has ecosystem-level knowledge/state, able to identify what cross-chain interactions are required to "move AssetX from ChainA to ChainB", depending on the actual assets and chains involved it can define things like:

    • reserve location of AssetX,
    • cross-chain path from ChainA to ChainB (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 @IkerAlus)

  3. 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:

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);
}

cc @xlc @franciscoaguirre

Polkadot-Forum commented 3 weeks ago

This issue has been mentioned on Polkadot Forum. There might be relevant details there:

https://forum.polkadot.network/t/rfc-xcm-asset-transfer-program-builder/8528/1

Polkadot-Forum commented 3 weeks ago

This issue has been mentioned on Polkadot Forum. There might be relevant details there:

https://forum.polkadot.network/t/rfc-xcm-asset-transfer-program-builder/8528/2

franciscoaguirre commented 3 weeks ago

I really like the API for this! We already have the XCM builder pattern as well but each method maps 1:1 to instructions. It would make a lot of sense to supercharge this with some utilities as you describe here. The define_asset is a good example of something very easy to add. That along with the context allows for nice behind-the-scenes reanchoring. A utility for better separating hops is also a good idea to add, since now building multiple hops requires you to start the process from scratch.

Polkadot-Forum commented 2 weeks ago

This issue has been mentioned on Polkadot Forum. There might be relevant details there:

https://forum.polkadot.network/t/rfc-xcm-asset-transfer-program-builder/8528/3