From fdb57ca09d1b23d75c557df4065579702884ae83 Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 14 Aug 2024 16:54:37 +0200 Subject: [PATCH] feat: switch functionality (#658) Fixes /~https://github.com/KILTprotocol/ticket/issues/3467. Not meant to be merged until all other stacked PRs will be merged into this. Adds the first round of features to the swap pallet, specifically it includes a swap pool that supports only the runtime local currency, swappable 1:1 (not configurable yet) with a configurable remote token. The XCM fees are paid in a local fungible token as injected by the runtime. The pallet also exposes two implementations of the `IsReserve` trait that must be added to the runtime to allow the configured tokens (to swap back to local currency and to pay for XCM fees) to be reserve-transferred to this chain. That code is not yet part of this PR. Additional TODOs for the pallet is both within the code where applicable, and in the pallet `README.md` file. --------- Co-authored-by: Adel Golghalyani <48685760+Ad96el@users.noreply.github.com> Co-authored-by: Adel Golghalyani --- .github/workflows/integration-tests.yml | 4 + Cargo.lock | 37 + Cargo.toml | 4 + dip-template/runtimes/dip-consumer/src/lib.rs | 8 +- dip-template/runtimes/dip-provider/src/lib.rs | 8 +- integration-tests/chopsticks/package.json | 6 +- .../chopsticks/src/network/assetHub.ts | 74 +- .../chopsticks/src/network/basilisk.ts | 58 ++ .../chopsticks/src/network/peregrine.ts | 120 +++ .../chopsticks/src/network/rococo.ts | 44 + .../chopsticks/src/network/utils.ts | 55 +- .../chopsticks/src/tests/index.ts | 112 ++- .../__snapshots__/fullFlowSwitch.test.ts.snap | 80 ++ .../__snapshots__/pauseSwitch.test.ts.snap | 170 ++++ .../__snapshots__/relayToken.test.ts.snap | 60 ++ .../__snapshots__/switchConfig.test.ts.snap | 71 ++ .../switchEkiltAgainstKilt.test.ts.snap | 57 ++ .../switchKiltAgainstEkilt.test.ts.snap | 55 ++ .../__snapshots__/trappedAssets.test.ts.snap | 189 ++++ .../tests/switchPallet/fullFlowSwitch.test.ts | 181 ++++ .../tests/switchPallet/pauseSwitch.test.ts | 399 ++++++++ .../src/tests/switchPallet/relayToken.test.ts | 172 ++++ .../tests/switchPallet/switchConfig.test.ts | 228 +++++ .../switchEkiltAgainstKilt.test.ts | 133 +++ .../switchKiltAgainstEkilt.test.ts | 129 +++ .../tests/switchPallet/trappedAssets.test.ts | 209 ++++ .../chopsticks/src/tests/utils.ts | 91 ++ ...awAssetsFromPeregrineAssetHub.test.ts.snap | 210 ++++ ...erveTransferAssetHubPeregrine.test.ts.snap | 194 ++++ ...ReserveTransferRelayPeregrine.test.ts.snap | 146 +++ ...ithdrawAssetsFromPeregrineAssetHub.test.ts | 103 ++ ...edReserveTransferAssetHubPeregrine.test.ts | 104 ++ ...mitedReserveTransferRelayPeregrine.test.ts | 75 ++ ...erveTransferAssetHubSpiritnet.test.ts.snap | 0 ...erveTransferSpiritnetAssetHub.test.ts.snap | 0 ...portTransferAssetHubSpiritnet.test.ts.snap | 0 ...portTransferSpiritnetAssetHub.test.ts.snap | 0 ...edReserveTransferAssetHubSpiritnet.test.ts | 22 +- ...edReserveTransferSpiritnetAssetHub.test.ts | 20 +- .../teleportTransferAssetHubSpiritnet.test.ts | 22 +- .../teleportTransferSpiritnetAssetHub.test.ts | 18 +- ...rveTransferSpiritnetHydraDxV2.test.ts.snap | 2 +- ...rveTransferSpiritnetHydraDxV3.test.ts.snap | 2 +- ...dReserveTransferSpiritnetHydraDxV2.test.ts | 7 +- ...dReserveTransferSpiritnetHydraDxV3.test.ts | 6 +- integration-tests/chopsticks/src/types.ts | 5 + integration-tests/chopsticks/src/utils.ts | 13 + integration-tests/chopsticks/yarn.lock | 655 +++++++++---- pallets/did/src/lib.rs | 5 +- pallets/pallet-asset-switch/Cargo.toml | 59 ++ pallets/pallet-asset-switch/README.md | 141 +++ .../pallet-asset-switch/src/benchmarking.rs | 321 ++++++ .../src/default_weights.rs | 252 +++++ pallets/pallet-asset-switch/src/lib.rs | 684 +++++++++++++ pallets/pallet-asset-switch/src/mock.rs | 321 ++++++ pallets/pallet-asset-switch/src/switch.rs | 168 ++++ .../src/tests/force_set_switch_pair.rs | 432 +++++++++ .../src/tests/force_unset_switch_pair.rs | 71 ++ pallets/pallet-asset-switch/src/tests/mod.rs | 25 + .../src/tests/pause_switch_pair.rs | 99 ++ .../src/tests/resume_switch_pair.rs | 99 ++ .../src/tests/set_switch_pair.rs | 393 ++++++++ .../pallet-asset-switch/src/tests/switch.rs | 473 +++++++++ .../src/tests/update_remote_xcm_fee.rs | 111 +++ pallets/pallet-asset-switch/src/traits.rs | 87 ++ pallets/pallet-asset-switch/src/try_state.rs | 65 ++ .../src/xcm/convert/mod.rs | 36 + .../pallet-asset-switch/src/xcm/match/mock.rs | 156 +++ .../pallet-asset-switch/src/xcm/match/mod.rs | 83 ++ .../src/xcm/match/tests.rs | 260 +++++ pallets/pallet-asset-switch/src/xcm/mod.rs | 35 + .../pallet-asset-switch/src/xcm/test_utils.rs | 60 ++ .../pallet-asset-switch/src/xcm/trade/mod.rs | 26 + .../trade/switch_pair_remote_asset/mock.rs | 166 ++++ .../xcm/trade/switch_pair_remote_asset/mod.rs | 266 +++++ .../tests/buy_weight.rs | 574 +++++++++++ .../switch_pair_remote_asset/tests/drop.rs | 179 ++++ .../switch_pair_remote_asset/tests/mod.rs | 21 + .../tests/refund_weight.rs | 682 +++++++++++++ .../src/xcm/trade/test_utils.rs | 31 + .../src/xcm/trade/xcm_fee_asset/mock.rs | 176 ++++ .../src/xcm/trade/xcm_fee_asset/mod.rs | 212 ++++ .../trade/xcm_fee_asset/tests/buy_weight.rs | 854 ++++++++++++++++ .../src/xcm/trade/xcm_fee_asset/tests/mod.rs | 20 + .../xcm_fee_asset/tests/refund_weight.rs | 725 ++++++++++++++ .../src/xcm/transact/mock.rs | 230 +++++ .../src/xcm/transact/mod.rs | 140 +++ .../src/xcm/transact/tests/deposit_asset.rs | 533 ++++++++++ .../src/xcm/transact/tests/mod.rs | 19 + .../src/xcm/transfer/mock.rs | 174 ++++ .../src/xcm/transfer/mod.rs | 26 + .../transfer/switch_pair_remote_asset/mod.rs | 74 ++ .../switch_pair_remote_asset/tests.rs | 558 +++++++++++ .../src/xcm/transfer/xcm_fee_asset/mod.rs | 74 ++ .../src/xcm/transfer/xcm_fee_asset/tests.rs | 592 ++++++++++++ runtime-api/asset-switch/Cargo.toml | 23 + runtime-api/asset-switch/README.md | 4 + runtime-api/asset-switch/src/lib.rs | 33 + runtimes/common/Cargo.toml | 4 + runtimes/common/src/asset_switch/mod.rs | 60 ++ .../common/src/asset_switch/runtime_api.rs | 27 + runtimes/common/src/constants.rs | 8 +- runtimes/common/src/lib.rs | 1 + runtimes/common/src/migrations.rs | 44 +- runtimes/common/src/xcm_config.rs | 9 +- runtimes/peregrine/Cargo.toml | 14 +- runtimes/peregrine/src/asset_switch/mod.rs | 118 +++ runtimes/peregrine/src/lib.rs | 186 +++- runtimes/peregrine/src/tests.rs | 21 +- runtimes/peregrine/src/weights/mod.rs | 2 + .../src/weights/pallet_asset_switch.rs | 235 +++++ .../peregrine/src/weights/pallet_assets.rs | 910 ++++++++++++++++++ runtimes/peregrine/src/weights/pallet_xcm.rs | 44 +- runtimes/peregrine/src/xcm_config.rs | 62 +- rust-toolchain.toml | 2 +- scripts/run_benches_for_pallets.sh | 4 +- scripts/run_benches_for_runtime.sh | 5 +- 117 files changed, 16624 insertions(+), 338 deletions(-) create mode 100644 integration-tests/chopsticks/src/network/basilisk.ts create mode 100644 integration-tests/chopsticks/src/network/peregrine.ts create mode 100644 integration-tests/chopsticks/src/network/rococo.ts create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/fullFlowSwitch.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/pauseSwitch.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/relayToken.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchConfig.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchEkiltAgainstKilt.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchKiltAgainstEkilt.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/trappedAssets.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/fullFlowSwitch.test.ts create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/pauseSwitch.test.ts create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/relayToken.test.ts create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/switchConfig.test.ts create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/switchEkiltAgainstKilt.test.ts create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/switchKiltAgainstEkilt.test.ts create mode 100644 integration-tests/chopsticks/src/tests/switchPallet/trappedAssets.test.ts create mode 100644 integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/initiateWithdrawAssetsFromPeregrineAssetHub.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/limitedReserveTransferAssetHubPeregrine.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/limitedReserveTransferRelayPeregrine.test.ts.snap create mode 100644 integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/initiateWithdrawAssetsFromPeregrineAssetHub.test.ts create mode 100644 integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/limitedReserveTransferAssetHubPeregrine.test.ts create mode 100644 integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/limitedReserveTransferRelayPeregrine.test.ts rename integration-tests/chopsticks/src/tests/xcm/assetHub/{ => spiritnet}/__snapshots__/limitedReserveTransferAssetHubSpiritnet.test.ts.snap (100%) rename integration-tests/chopsticks/src/tests/xcm/assetHub/{ => spiritnet}/__snapshots__/limitedReserveTransferSpiritnetAssetHub.test.ts.snap (100%) rename integration-tests/chopsticks/src/tests/xcm/assetHub/{ => spiritnet}/__snapshots__/teleportTransferAssetHubSpiritnet.test.ts.snap (100%) rename integration-tests/chopsticks/src/tests/xcm/assetHub/{ => spiritnet}/__snapshots__/teleportTransferSpiritnetAssetHub.test.ts.snap (100%) rename integration-tests/chopsticks/src/tests/xcm/assetHub/{ => spiritnet}/limitedReserveTransferAssetHubSpiritnet.test.ts (73%) rename integration-tests/chopsticks/src/tests/xcm/assetHub/{ => spiritnet}/limitedReserveTransferSpiritnetAssetHub.test.ts (75%) rename integration-tests/chopsticks/src/tests/xcm/assetHub/{ => spiritnet}/teleportTransferAssetHubSpiritnet.test.ts (73%) rename integration-tests/chopsticks/src/tests/xcm/assetHub/{ => spiritnet}/teleportTransferSpiritnetAssetHub.test.ts (67%) create mode 100644 integration-tests/chopsticks/src/types.ts create mode 100644 pallets/pallet-asset-switch/Cargo.toml create mode 100644 pallets/pallet-asset-switch/README.md create mode 100644 pallets/pallet-asset-switch/src/benchmarking.rs create mode 100644 pallets/pallet-asset-switch/src/default_weights.rs create mode 100644 pallets/pallet-asset-switch/src/lib.rs create mode 100644 pallets/pallet-asset-switch/src/mock.rs create mode 100644 pallets/pallet-asset-switch/src/switch.rs create mode 100644 pallets/pallet-asset-switch/src/tests/force_set_switch_pair.rs create mode 100644 pallets/pallet-asset-switch/src/tests/force_unset_switch_pair.rs create mode 100644 pallets/pallet-asset-switch/src/tests/mod.rs create mode 100644 pallets/pallet-asset-switch/src/tests/pause_switch_pair.rs create mode 100644 pallets/pallet-asset-switch/src/tests/resume_switch_pair.rs create mode 100644 pallets/pallet-asset-switch/src/tests/set_switch_pair.rs create mode 100644 pallets/pallet-asset-switch/src/tests/switch.rs create mode 100644 pallets/pallet-asset-switch/src/tests/update_remote_xcm_fee.rs create mode 100644 pallets/pallet-asset-switch/src/traits.rs create mode 100644 pallets/pallet-asset-switch/src/try_state.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/convert/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/match/mock.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/match/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/match/tests.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/test_utils.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/mock.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/buy_weight.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/drop.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/refund_weight.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/test_utils.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mock.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/buy_weight.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/refund_weight.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transact/mock.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transact/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transact/tests/deposit_asset.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transact/tests/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transfer/mock.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transfer/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transfer/switch_pair_remote_asset/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transfer/switch_pair_remote_asset/tests.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transfer/xcm_fee_asset/mod.rs create mode 100644 pallets/pallet-asset-switch/src/xcm/transfer/xcm_fee_asset/tests.rs create mode 100644 runtime-api/asset-switch/Cargo.toml create mode 100644 runtime-api/asset-switch/README.md create mode 100644 runtime-api/asset-switch/src/lib.rs create mode 100644 runtimes/common/src/asset_switch/mod.rs create mode 100644 runtimes/common/src/asset_switch/runtime_api.rs create mode 100644 runtimes/peregrine/src/asset_switch/mod.rs create mode 100644 runtimes/peregrine/src/weights/pallet_asset_switch.rs create mode 100644 runtimes/peregrine/src/weights/pallet_assets.rs diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ef627e2c0..55dccfc54 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -29,6 +29,7 @@ jobs: env: working-dir: ./integration-tests/chopsticks CI: "true" + PEREGRINE_WASM_OVERRIDE: ../../target/debug/wbuild/peregrine-runtime/peregrine_runtime.wasm defaults: run: working-directory: ${{ env.working-dir }} @@ -49,6 +50,9 @@ jobs: - name: Linting run: yarn lint + + - name: Build Runtime + run: cargo build -p peregrine-runtime - name: Test Suite run: yarn test:CI diff --git a/Cargo.lock b/Cargo.lock index df9c2935a..ecae77166 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6906,6 +6906,37 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-asset-switch" +version = "1.14.0-dev" +dependencies = [ + "env_logger 0.10.2", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-keystore", + "sp-runtime", + "sp-std", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", +] + +[[package]] +name = "pallet-asset-switch-runtime-api" +version = "1.14.0-dev" +dependencies = [ + "parity-scale-codec", + "sp-api", + "sp-std", +] + [[package]] name = "pallet-asset-tx-payment" version = "29.0.0" @@ -8656,6 +8687,7 @@ name = "peregrine-runtime" version = "1.14.0-dev" dependencies = [ "attestation", + "cfg-if", "ctype", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", @@ -8669,6 +8701,7 @@ dependencies = [ "cumulus-primitives-utility", "delegation", "did", + "enum-iterator", "frame-benchmarking", "frame-executive", "frame-support", @@ -8683,6 +8716,9 @@ dependencies = [ "kilt-runtime-api-staking", "kilt-support", "log", + "pallet-asset-switch", + "pallet-asset-switch-runtime-api", + "pallet-assets", "pallet-aura", "pallet-authorship", "pallet-balances", @@ -10868,6 +10904,7 @@ dependencies = [ "kilt-dip-primitives", "kilt-support", "log", + "pallet-assets", "pallet-authorship", "pallet-balances", "pallet-deposit-storage", diff --git a/Cargo.toml b/Cargo.toml index b2802bd27..f9b904cfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ attestation = { path = "pallets/attestation", default-features = fals ctype = { path = "pallets/ctype", default-features = false } delegation = { path = "pallets/delegation", default-features = false } did = { path = "pallets/did", default-features = false } +pallet-asset-switch = { path = "pallets/pallet-asset-switch", default-features = false } pallet-configuration = { path = "pallets/pallet-configuration", default-features = false } pallet-deposit-storage = { path = "pallets/pallet-deposit-storage", default-features = false } pallet-did-lookup = { path = "pallets/pallet-did-lookup", default-features = false } @@ -85,6 +86,7 @@ kilt-runtime-api-did = { path = "runtime-api/did", default-featur kilt-runtime-api-dip-provider = { path = "runtime-api/dip-provider", default-features = false } kilt-runtime-api-public-credentials = { path = "runtime-api/public-credentials", default-features = false } kilt-runtime-api-staking = { path = "runtime-api/staking", default-features = false } +pallet-asset-switch-runtime-api = { path = "runtime-api/asset-switch", default-features = false } # Internal KILT runtimes (with default disabled) kestrel-runtime = { path = "runtimes/kestrel", default-features = false } @@ -125,6 +127,7 @@ rococo-emulated-chain = { git = "/~https://github.com/paritytech/polka xcm-emulator = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } # Substrate (with default disabled) + frame-benchmarking = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } frame-benchmarking-cli = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } frame-executive = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } @@ -132,6 +135,7 @@ frame-support = { git = "/~https://github.com/parityt frame-system = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } frame-system-rpc-runtime-api = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } frame-try-runtime = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } +pallet-assets = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } pallet-aura = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } pallet-authorship = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } pallet-balances = { git = "/~https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-crates-io-v1.7.0" } diff --git a/dip-template/runtimes/dip-consumer/src/lib.rs b/dip-template/runtimes/dip-consumer/src/lib.rs index e75cd1fa3..38881a549 100644 --- a/dip-template/runtimes/dip-consumer/src/lib.rs +++ b/dip-template/runtimes/dip-consumer/src/lib.rs @@ -230,11 +230,11 @@ impl frame_system::Config for Runtime { type Version = Version; } -/// Maximum number of blocks simultaneously accepted by the Runtime, not yet included into the -/// relay chain. +/// Maximum number of blocks simultaneously accepted by the Runtime, not yet +/// included into the relay chain. const UNINCLUDED_SEGMENT_CAPACITY: u32 = 1; -/// How many parachain blocks are processed by the relay chain per parent. Limits the number of -/// blocks authored per slot. +/// How many parachain blocks are processed by the relay chain per parent. +/// Limits the number of blocks authored per slot. const BLOCK_PROCESSING_VELOCITY: u32 = 1; /// Relay chain slot duration, in milliseconds. const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; diff --git a/dip-template/runtimes/dip-provider/src/lib.rs b/dip-template/runtimes/dip-provider/src/lib.rs index dc4d316a3..9f3139a33 100644 --- a/dip-template/runtimes/dip-provider/src/lib.rs +++ b/dip-template/runtimes/dip-provider/src/lib.rs @@ -231,11 +231,11 @@ impl frame_system::Config for Runtime { type Version = Version; } -/// Maximum number of blocks simultaneously accepted by the Runtime, not yet included into the -/// relay chain. +/// Maximum number of blocks simultaneously accepted by the Runtime, not yet +/// included into the relay chain. const UNINCLUDED_SEGMENT_CAPACITY: u32 = 1; -/// How many parachain blocks are processed by the relay chain per parent. Limits the number of -/// blocks authored per slot. +/// How many parachain blocks are processed by the relay chain per parent. +/// Limits the number of blocks authored per slot. const BLOCK_PROCESSING_VELOCITY: u32 = 1; /// Relay chain slot duration, in milliseconds. const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; diff --git a/integration-tests/chopsticks/package.json b/integration-tests/chopsticks/package.json index 8145f6a93..1d8c46dbe 100644 --- a/integration-tests/chopsticks/package.json +++ b/integration-tests/chopsticks/package.json @@ -19,6 +19,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-config-standard-with-typescript": "^43.0.1", "eslint-plugin-import": "^2.25.2", + "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^6.0.0", @@ -26,8 +27,7 @@ "ts-node": "^10.9.2", "tsx": "^4.7.1", "typescript": "*", - "vitest": "^1.4.0", - "eslint-plugin-jsx-a11y": "^6.8.0" + "vitest": "^1.4.0" }, "scripts": { "ts-check": "tsc --noEmit", @@ -35,6 +35,6 @@ "lint:fix": "eslint --fix src && prettier --write src", "clean": "rm -rf ./db", "test": "LOG_LEVEL=error vitest", - "test:CI": "vitest --bail 0 --no-file-parallelism" + "test:CI": "vitest --no-file-parallelism --retry 3" } } diff --git a/integration-tests/chopsticks/src/network/assetHub.ts b/integration-tests/chopsticks/src/network/assetHub.ts index e4ecc0b83..cbc3f0891 100644 --- a/integration-tests/chopsticks/src/network/assetHub.ts +++ b/integration-tests/chopsticks/src/network/assetHub.ts @@ -19,6 +19,43 @@ export const getSetupOptions = ({ blockNumber, }) as SetupOption +/// AssetHub has no own coin. Teleported dots are used as the native token. +export function assignDotTokensToAccountsAsStorage(addr: string[], balance: bigint = initialBalanceDOT) { + return { + System: { + Account: addr.map((address) => [[address], { providers: 1, data: { free: balance.toString() } }]), + }, + } +} + +export function createForeignAsset(manager: string, assetId = eKiltLocation) { + return { + foreignAssets: { + asset: [ + [ + [assetId], + { + owner: manager, + issuer: manager, + admin: manager, + freezer: manager, + // Just make it big enough + supply: '10000000000000000000000000000', + deposit: 0, + minBalance: 0, + isSufficient: false, + accounts: 0, + sufficients: 0, + approvals: 0, + status: 'Live', + }, + ], + ], + }, + } +} + +/// Assigns KSM to an account export function assignKSMtoAccounts(addr: string[], balance: bigint = initialBalanceDOT) { return { foreignAssets: { @@ -35,11 +72,20 @@ export function assignKSMtoAccounts(addr: string[], balance: bigint = initialBal } } -/// AssetHub has no own coin. Teleported dots are used as the native token. -export function assignDotTokensToAccounts(addr: string[], balance: bigint = initialBalanceDOT) { +/// Assigns the foreign asset to the accounts. +/// Does not check if supply is matching the sum of the account balances. +export function assignForeignAssetToAccounts(accountInfo: [string, bigint][], assetId = eKiltLocation) { return { - System: { - Account: addr.map((address) => [[address], { providers: 1, data: { free: balance.toString() } }]), + foreignAssets: { + account: accountInfo.map(([account, balance]) => [ + [assetId, account], + { + balance: balance, + status: 'Liquid', + reason: 'Consumer', + extra: null, + }, + ]), }, } } @@ -59,6 +105,26 @@ export const KSMAssetLocation = { // Sibling Sovereign Account export const sovereignAccountOnSiblingChains = '4qXPdpimHh8TR24RSk994yVzxx4TLfvKj5i1qH5puvWmfAqy' +/// Native token in AssetHub +export const nativeTokenLocation = { parents: 1, interior: 'Here' } + +export const eKiltLocation = { + parents: 2, + interior: { + X2: [ + { + GlobalConsensus: { Ethereum: { chainId: 11155111 } }, + }, + { + AccountKey20: { + network: null, + key: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + }, + }, + ], + }, +} + export async function getContext(): Promise { const options = getSetupOptions({}) return setupContext(options) diff --git a/integration-tests/chopsticks/src/network/basilisk.ts b/integration-tests/chopsticks/src/network/basilisk.ts new file mode 100644 index 000000000..d5bd1583c --- /dev/null +++ b/integration-tests/chopsticks/src/network/basilisk.ts @@ -0,0 +1,58 @@ +import { setupContext, SetupOption } from '@acala-network/chopsticks-testing' +import type { Config } from './types.js' +import { initialBalanceDOT, initialBalanceHDX, initialBalanceKILT, toNumber } from '../utils.js' + +/// Options used to create the HydraDx context +export const options: SetupOption = { + endpoint: process.env.BASILISK_WS || ['wss://basilisk-rococo-rpc.play.hydration.cloud'], + db: './db/basilisk.db.sqlite', + port: toNumber(process.env.HYDRADX_PORT) || 9005, +} + +// On Basilisk, there is only KSM. We use that currency and treat it as ROC, since the location is {parents: 1, interior: Here} +export const dotTokenId = 5 + +export const eKILTTokenId = 90 + +/// Assigns the native tokens to an accounts +export function assignNativeTokensToAccounts(addr: string[], balance: bigint = initialBalanceHDX) { + return { + System: { + Account: addr.map((address) => [[address], { providers: 1, data: { free: balance } }]), + }, + } +} + +/// Assigns ROCs tokens to an accounts +export function assignRocTokensToAccounts(addr: string[], balance: bigint = initialBalanceDOT) { + return { + Tokens: { + Accounts: addr.map((address) => [[address, dotTokenId], { free: balance }]), + }, + } +} + +export function createRemoteAsset( + addr: string[], + assetLocation: Record, + balance: bigint = initialBalanceKILT +) { + return { + assetRegistry: { + assetIds: [[['eKILT'], 90]], + assetLocations: [[[90], assetLocation]], + assetMetadataMap: [[[90], { symbol: 'eKILT', decimals: 15 }]], + }, + + tokens: { + accounts: addr.map((acc) => [[acc, 90], { free: balance }]), + }, + } +} + +/// Basilisk ParaId +export const paraId = 2090 + +export async function getContext(): Promise { + return setupContext(options) +} diff --git a/integration-tests/chopsticks/src/network/peregrine.ts b/integration-tests/chopsticks/src/network/peregrine.ts new file mode 100644 index 000000000..ed38f4b39 --- /dev/null +++ b/integration-tests/chopsticks/src/network/peregrine.ts @@ -0,0 +1,120 @@ +import { SetupOption, setupContext } from '@acala-network/chopsticks-testing' + +import type { Config } from './types.js' +import { ROC, initialBalanceKILT, initialBalanceROC, toNumber } from '../utils.js' +import { AssetSwitchSupplyParameters } from '../types.js' + +/// Options used to create the Peregrine context +const options = { + endpoint: process.env.PEREGRINE_WS || 'wss://peregrine.kilt.io', + db: './db/peregrine.db.sqlite', + port: toNumber(process.env.PEREGRINE_PORT) || 9004, + wasmOverride: process.env.PEREGRINE_WASM_OVERRIDE || undefined, +} as SetupOption + +/// Assigns the native tokens to an accounts +export function assignNativeTokensToAccounts(addr: string[], balance: bigint = initialBalanceKILT) { + return { + System: { + Account: addr.map((address) => [[address], { providers: 1, data: { free: balance } }]), + }, + } +} + +export function setSafeXcmVersion4() { + return { + polkadotXcm: { + safeXcmVersion: 4, + }, + } +} + +export function createAndAssignRocs(manager: string, addr: string[], balance: bigint = initialBalanceROC) { + return { + fungibles: { + asset: [ + [ + [ROC_LOCATION], + { + owner: manager, + issuer: manager, + admin: manager, + freezer: manager, + supply: balance * BigInt(addr.length), + deposit: 0, + minBalance: 1, + isSufficient: false, + accounts: addr.length, + sufficients: 0, + approvals: 0, + status: 'Live', + }, + ], + ], + account: addr.map((acc) => [ + [{ parents: 1, interior: 'here' }, acc], + { balance: balance, status: 'Liquid', reason: 'Consumer', extra: null }, + ]), + }, + } +} + +export function setSudoKey(sudoKey: string) { + return { + Sudo: { + key: sudoKey, + }, + } +} + +export function setSwitchPair( + parameters: AssetSwitchSupplyParameters, + remoteAssetId: Record, + remoteXcmFee: Record, + remoteReserveLocation: Record, + poolAccountId: string = initialPoolAccountId, + status: 'Running' | 'Paused' = 'Running' +) { + return { + assetSwitchPool1: { + SwitchPair: { + poolAccountId, + remoteAssetSovereignTotalBalance: parameters.sovereignSupply, + remoteAssetCirculatingSupply: parameters.circulatingSupply, + remoteAssetTotalSupply: parameters.totalSupply, + remoteAssetId, + remoteXcmFee, + remoteReserveLocation, + status, + }, + }, + // the pool account needs at least as much fund to cover the circulating supply. Give him exactly that amount + ED. + System: { + Account: [[[poolAccountId], { providers: 1, data: { free: parameters.circulatingSupply } }]], + }, + } +} + +/// Peregrine ParaId +export const paraId = 2086 +export const PILT = { Concrete: { parents: 0, interior: 'Here' } } +export const ROC_LOCATION = { + parents: 1, + interior: 'Here', +} +// 0.1 ROC as remote fee +export const remoteFee = ROC / BigInt(10) + +/// Sibling sovereign account for other chains +export const sovereignAccountAsSibling = '5Eg2fnshxV9kofpcNEFE7azHLAjcCtpNkbsH3kkWZasYUVKs' +// ED on Peregrine +export const existentialDeposit = BigInt('10000000000000') + +export const initialPoolAccountId = '4nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS' + +export async function getContext(): Promise { + return setupContext(options) +} + +export const treasuryAccount = '5EYCAe5hitmhtZcXaCrygrqw4kbmJ8bGm15PBuwUdDdQTUgY' +export const parachainStakingRewards = BigInt('760514107536694') diff --git a/integration-tests/chopsticks/src/network/rococo.ts b/integration-tests/chopsticks/src/network/rococo.ts new file mode 100644 index 000000000..5fde01164 --- /dev/null +++ b/integration-tests/chopsticks/src/network/rococo.ts @@ -0,0 +1,44 @@ +import { setupContext, SetupOption } from '@acala-network/chopsticks-testing' +import type { Config } from './types.js' +import { initialBalanceROC, toNumber } from '../utils.js' + +/// Options used to create the HydraDx context +export const options: SetupOption = { + endpoint: process.env.ROCOCO_WS || ['wss://rococo-rpc.polkadot.io'], + db: './db/rococo.db.sqlite', + port: toNumber(process.env.ROCOCO_PORT) || 8999, +} + +/// Assigns the native tokens to an accounts +export function assignNativeTokensToAccounts(addr: string[], balance: bigint = initialBalanceROC) { + return { + System: { + Account: addr.map((address) => [[address], { providers: 1, data: { free: balance } }]), + }, + } +} + +export function setSudoKey(sudo: string) { + return { + Sudo: { + key: sudo, + }, + } +} + +export function removeDisputesAndMessageQueues() { + return { + ParasDisputes: { + // those can makes block building super slow + $removePrefix: ['disputes'], + }, + Dmp: { + // clear existing dmp to avoid impact test result + $removePrefix: ['downwardMessageQueues'], + }, + } +} + +export async function getContext(): Promise { + return setupContext(options) +} diff --git a/integration-tests/chopsticks/src/network/utils.ts b/integration-tests/chopsticks/src/network/utils.ts index 144daab0b..00fceb46e 100644 --- a/integration-tests/chopsticks/src/network/utils.ts +++ b/integration-tests/chopsticks/src/network/utils.ts @@ -1,4 +1,4 @@ -export function getSiblingLocation(paraId: number) { +export function getSiblingLocationV3(paraId: number) { return { parents: 1, interior: { @@ -7,6 +7,26 @@ export function getSiblingLocation(paraId: number) { } } +export function getSiblingLocationV4(paraId: number) { + return { + V4: { + parents: 1, + interior: { + X1: [{ Parachain: paraId }], + }, + }, + } +} + +export function getChildLocation(paraId: number) { + return { + parents: 0, + interior: { + X1: { Parachain: paraId }, + }, + } +} + export function getParentLocation() { return { parents: 1, @@ -45,9 +65,40 @@ export function getAccountLocationV3(addr: string) { } } -export function getNativeAssetIdLocation(amount: bigint | string) { +export function getAccountLocationV4(addr: string) { + return { + V4: { + parents: 0, + interior: { + X1: [ + { + AccountId32: { + id: addr, + }, + }, + ], + }, + }, + } +} + +export function getNativeAssetIdLocationV3(amount: bigint | string) { return { id: { Concrete: { parents: 0, interior: 'Here' } }, fun: { Fungible: amount }, } } + +export function getRelayNativeAssetIdLocationV3(amount: bigint | string) { + return { + id: { Concrete: { parents: 1, interior: 'Here' } }, + fun: { Fungible: amount }, + } +} + +export function getRelayNativeAssetIdLocationV4(amount: bigint | string) { + return { + id: { parents: 1, interior: 'Here' }, + fun: { Fungible: amount }, + } +} diff --git a/integration-tests/chopsticks/src/tests/index.ts b/integration-tests/chopsticks/src/tests/index.ts index 80c6389b8..ba37c72d6 100644 --- a/integration-tests/chopsticks/src/tests/index.ts +++ b/integration-tests/chopsticks/src/tests/index.ts @@ -1,4 +1,4 @@ -import { beforeAll, afterAll } from 'vitest' +import { beforeEach, afterEach } from 'vitest' import { connectParachains, connectVertical } from '@acala-network/chopsticks' import { setTimeout } from 'timers/promises' @@ -6,25 +6,39 @@ import * as SpiritnetConfig from '../network/spiritnet.js' import * as PolkadotConfig from '../network/polkadot.js' import * as HydraDxConfig from '../network/hydraDx.js' import * as AssetHubConfig from '../network/assetHub.js' +import * as RococoConfig from '../network/rococo.js' +import * as BasiliskConfig from '../network/basilisk.js' +import * as PeregrineConfig from '../network/peregrine.js' import type { Config } from '../network/types.js' export let spiritnetContext: Config export let hydradxContext: Config export let polkadotContext: Config -export let assetHubContext: Config +export let assethubContext: Config +export let peregrineContext: Config +export let rococoContext: Config +export let basiliskContext: Config -beforeAll(async () => { +beforeEach(async () => { spiritnetContext = await SpiritnetConfig.getContext() hydradxContext = await HydraDxConfig.getContext() polkadotContext = await PolkadotConfig.getContext() - assetHubContext = await AssetHubConfig.getContext() - - // Setup network + assethubContext = await AssetHubConfig.getContext() + rococoContext = await RococoConfig.getContext() + peregrineContext = await PeregrineConfig.getContext() + basiliskContext = await BasiliskConfig.getContext() + // Setup Polkadot network await connectVertical(polkadotContext.chain, spiritnetContext.chain) await connectVertical(polkadotContext.chain, hydradxContext.chain) - await connectVertical(polkadotContext.chain, assetHubContext.chain) - await connectParachains([spiritnetContext.chain, hydradxContext.chain, assetHubContext.chain]) + await connectVertical(polkadotContext.chain, assethubContext.chain) + await connectParachains([spiritnetContext.chain, hydradxContext.chain, assethubContext.chain]) + + // Setup Rococo network + await connectVertical(rococoContext.chain, assethubContext.chain) + await connectVertical(rococoContext.chain, peregrineContext.chain) + await connectVertical(rococoContext.chain, basiliskContext.chain) + await connectParachains([peregrineContext.chain, basiliskContext.chain, assethubContext.chain]) const newBlockConfig = { count: 2 } // fixes api runtime disconnect warning @@ -34,22 +48,32 @@ beforeAll(async () => { polkadotContext.dev.newBlock(newBlockConfig), spiritnetContext.dev.newBlock(newBlockConfig), hydradxContext.dev.newBlock(newBlockConfig), - assetHubContext.dev.newBlock(newBlockConfig), + assethubContext.dev.newBlock(newBlockConfig), + rococoContext.dev.newBlock(newBlockConfig), + peregrineContext.dev.newBlock(newBlockConfig), + basiliskContext.dev.newBlock(newBlockConfig), ]) -}, 300_000) +}, 90_000) + +afterEach(async () => { + // fixes api runtime disconnect warning -afterAll(async () => { try { - await setTimeout(50) await Promise.all([ spiritnetContext.teardown(), hydradxContext.teardown(), polkadotContext.teardown(), - assetHubContext.teardown(), + assethubContext.teardown(), + rococoContext.teardown(), + peregrineContext.teardown(), + basiliskContext.teardown(), ]) - } catch (e) { - console.error(e) + } catch (error) { + if (!(error instanceof TypeError)) { + console.error(error) + } } + await setTimeout(50) }) export async function getFreeBalanceSpiritnet(account: string): Promise { @@ -57,6 +81,64 @@ export async function getFreeBalanceSpiritnet(account: string): Promise return accountInfo.data.free.toBigInt() } +export async function getFreeBalancePeregrine(account: string): Promise { + const accountInfo = await peregrineContext.api.query.system.account(account) + return accountInfo.data.free.toBigInt() +} + +export async function getFreeBalancePeregrineAt(account: string, at: number): Promise { + const blockHash = await peregrineContext.api.rpc.chain.getBlockHash(at) + const api = await peregrineContext.api.at(blockHash) + const accountInfo = await api.query.system.account(account) + return accountInfo.data.free.toBigInt() +} + +export async function getCurrentBlockNumber(context: Config): Promise { + const blockNumber = await context.api.query.system.number() + return blockNumber.toNumber() +} + +export async function getFreeRocPeregrine(account: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const accountInfo: any = await peregrineContext.api.query.fungibles.account( + AssetHubConfig.nativeTokenLocation, + account + ) + if (accountInfo.isNone) { + return BigInt(0) + } + return accountInfo.unwrap().balance.toBigInt() +} + +export async function getFreeRocAssetHub(account: string): Promise { + const accountInfo = await assethubContext.api.query.system.account(account) + return accountInfo.data.free.toBigInt() +} + +export async function getRemoteLockedSupply(): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const switchPairInfo: any = await peregrineContext.api.query.assetSwitchPool1.switchPair() + + if (switchPairInfo.isNone) { + return BigInt(0) + } + + return switchPairInfo.unwrap().remoteAssetSovereignTotalBalance.toBigInt() +} + +export async function getFreeEkiltAssetHub(account: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const accountInfo: any = await assethubContext.api.query.foreignAssets.account( + AssetHubConfig.eKiltLocation, + account + ) + if (accountInfo.isNone) { + return BigInt(0) + } + + return accountInfo.unwrap().balance.toBigInt() +} + export async function getFreeBalanceHydraDxKilt(account: string): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const accountInfo: any = await hydradxContext.api.query.tokens.accounts(account, HydraDxConfig.kiltTokenId) diff --git a/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/fullFlowSwitch.test.ts.snap b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/fullFlowSwitch.test.ts.snap new file mode 100644 index 000000000..f2d53fcf3 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/fullFlowSwitch.test.ts.snap @@ -0,0 +1,80 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Full e2e tests > receiver Peregrine::assetSwitchPool1::[LocalToRemoteSwitchExecuted] 1`] = ` +[ + { + "data": { + "amount": 50000000000000000, + "from": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "to": { + "V4": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + }, + "method": "LocalToRemoteSwitchExecuted", + "section": "assetSwitchPool1", + }, +] +`; + +exports[`Full e2e tests > sender AssetHub::foreignAssets::[Transferred] 1`] = ` +[ + { + "data": { + "amount": 50000000000000000, + "assetId": { + "interior": { + "X2": [ + { + "GlobalConsensus": { + "Ethereum": { + "chainId": "(rounded 11000000)", + }, + }, + }, + { + "AccountKey20": { + "key": "0x06012c8cf97bead5deae237070f9587f8e7a266d", + "network": null, + }, + }, + ], + }, + "parents": 2, + }, + "from": "15jSz35ugoWTc61xHPoxEkHte4o7UanKCk1gx1dizA8yuNs8", + "to": "13cKp88mpGREFCq8KsJEFjpSBnjFuCNWq6bmD3js7fu4f66e", + }, + "method": "Transferred", + "section": "foreignAssets", + }, +] +`; + +exports[`Full e2e tests > sender Peregrine::fungibles::[Burned] 1`] = ` +[ + { + "data": { + "assetId": { + "interior": "Here", + "parents": 1, + }, + "balance": 1000000000000, + "owner": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + }, + "method": "Burned", + "section": "fungibles", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/pauseSwitch.test.ts.snap b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/pauseSwitch.test.ts.snap new file mode 100644 index 000000000..5463ee5c7 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/pauseSwitch.test.ts.snap @@ -0,0 +1,170 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Send ROCs while switch paused > receiver Peregrine::messageQueue::[Processed] 1`] = ` +[ + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": 1000, + }, + "success": false, + "weightUsed": { + "proofSize": 0, + "refTime": 600000000, + }, + }, + "method": "Processed", + "section": "messageQueue", + }, +] +`; + +exports[`Send ROCs while switch paused > receiver Peregrine::polkadotXcm::[AssetsTrapped] 1`] = ` +[ + { + "data": { + "assets": { + "V4": [ + { + "fun": { + "Fungible": 1000000000000, + }, + "id": { + "interior": "Here", + "parents": 1, + }, + }, + ], + }, + "hash_": "(hash)", + "origin": { + "interior": { + "X1": [ + { + "Parachain": 1000, + }, + ], + }, + "parents": 1, + }, + }, + "method": "AssetsTrapped", + "section": "polkadotXcm", + }, +] +`; + +exports[`Switch ePILTs against PILTs while paused > receiver Peregrine::messageQueue::[Processed] 1`] = ` +[ + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": 1000, + }, + "success": false, + "weightUsed": { + "proofSize": 0, + "refTime": 600000000, + }, + }, + "method": "Processed", + "section": "messageQueue", + }, + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": 1000, + }, + "success": true, + "weightUsed": { + "proofSize": 0, + "refTime": 400000000, + }, + }, + "method": "Processed", + "section": "messageQueue", + }, +] +`; + +exports[`Switch ePILTs against PILTs while paused > receiver Peregrine::polkadotXcm::[AssetsTrapped] 1`] = ` +[ + { + "data": { + "assets": { + "V4": [ + { + "fun": { + "Fungible": 25000000000000000, + }, + "id": { + "interior": { + "X2": [ + { + "GlobalConsensus": { + "Ethereum": { + "chainId": "(rounded 11000000)", + }, + }, + }, + { + "AccountKey20": { + "key": "0x06012c8cf97bead5deae237070f9587f8e7a266d", + "network": null, + }, + }, + ], + }, + "parents": 2, + }, + }, + ], + }, + "hash_": "(hash)", + "origin": { + "interior": { + "X1": [ + { + "Parachain": 1000, + }, + ], + }, + "parents": 1, + }, + }, + "method": "AssetsTrapped", + "section": "polkadotXcm", + }, +] +`; + +exports[`Switch ePILTs against PILTs while paused > sender Peregrine::assetSwitchPool1::[LocalToRemoteSwitchExecuted] 1`] = ` +[ + { + "data": { + "amount": 50000000000000000, + "from": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "to": { + "V4": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + }, + "method": "LocalToRemoteSwitchExecuted", + "section": "assetSwitchPool1", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/relayToken.test.ts.snap b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/relayToken.test.ts.snap new file mode 100644 index 000000000..ddfd6434f --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/relayToken.test.ts.snap @@ -0,0 +1,60 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Send DOTs from basilisk 2 Peregrine > receiver Peregrine::messageQueue::[ProcessingFailed] 1`] = ` +[ + { + "data": { + "error": "Unsupported", + "id": "(hash)", + "origin": "Parent", + }, + "method": "ProcessingFailed", + "section": "messageQueue", + }, +] +`; + +exports[`User gets dusted with ROCs > local Peregrine::balances::[Transfer] native asset 1`] = ` +[ + { + "data": { + "amount": "(rounded 100000000000000000)", + "from": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "to": "4rsmbFBYpVmWE2LHRsSZKxf22a8cbJLxMZqvVGkGkSDmDBcr", + }, + "method": "Transfer", + "section": "balances", + }, +] +`; + +exports[`User gets dusted with ROCs > local balances::fungibles::[DustLost] 1`] = ` +[ + { + "data": { + "account": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "amount": "(rounded 51000000000)", + }, + "method": "DustLost", + "section": "balances", + }, +] +`; + +exports[`User transfers all of his dots > local Peregrine::fungibles::[Transferred] asset {"parents":1,"interior":"Here"} 1`] = ` +[ + { + "data": { + "amount": 100000000000000, + "assetId": { + "interior": "Here", + "parents": 1, + }, + "from": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "to": "4rsmbFBYpVmWE2LHRsSZKxf22a8cbJLxMZqvVGkGkSDmDBcr", + }, + "method": "Transferred", + "section": "fungibles", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchConfig.test.ts.snap b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchConfig.test.ts.snap new file mode 100644 index 000000000..fb1339c94 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchConfig.test.ts.snap @@ -0,0 +1,71 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Send eKILT from other reserve location > receiver Peregrine::messageQueue::[Processed] 1`] = ` +[ + { + "data": { + "error": "Unsupported", + "id": "(hash)", + "origin": "Parent", + }, + "method": "ProcessingFailed", + "section": "messageQueue", + }, +] +`; + +exports[`Send eKILT while switch Pair does not exist > receiver Peregrine::messageQueue::[Processed] 1`] = `[]`; + +exports[`Switch KILTs against EKILTs no enough DOTs on AH > sender Peregrine::assetSwitchPool1::[LocalToRemoteSwitchExecuted] 1`] = ` +[ + { + "data": { + "amount": 50000000000000000, + "from": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "to": { + "V4": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + }, + "method": "LocalToRemoteSwitchExecuted", + "section": "assetSwitchPool1", + }, +] +`; + +exports[`Switch KILTs against EKILTs no enough DOTs on AH > sender Peregrine::balances::[Transfer] 1`] = ` +[ + { + "data": { + "amount": 50000000000000000, + "from": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "to": "4nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS", + }, + "method": "Transfer", + "section": "balances", + }, +] +`; + +exports[`Switch KILTs against EKILTs no enough DOTs on AH > sender Peregrine::xcmpQueue::[XcmpMessageSent] 1`] = ` +[ + { + "data": { + "messageHash": "(hash)", + }, + "method": "XcmpMessageSent", + "section": "xcmpQueue", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchEkiltAgainstKilt.test.ts.snap b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchEkiltAgainstKilt.test.ts.snap new file mode 100644 index 000000000..616554455 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchEkiltAgainstKilt.test.ts.snap @@ -0,0 +1,57 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Switch ePILTs against PILTS on Peregrine > receiver Peregrine::assetSwitchPool1::[RemoteToLocalSwitchExecuted] 1`] = ` +[ + { + "data": { + "amount": "(rounded 50000000000000000)", + "to": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + }, + "method": "RemoteToLocalSwitchExecuted", + "section": "assetSwitchPool1", + }, +] +`; + +exports[`Switch ePILTs against PILTS on Peregrine > receiver Peregrine::balances::[Transfer] 1`] = ` +[ + { + "data": { + "amount": "(rounded 50000000000000000)", + "from": "4nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS", + "to": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + }, + "method": "Transfer", + "section": "balances", + }, + { + "data": { + "amount": "(rounded 35000)", + "from": "4nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS", + "to": "4qPZ8fv6BjGoGKzfx5LtBFnEUp2b5Q5C1ErrjBNGmoFTLNHG", + }, + "method": "Transfer", + "section": "balances", + }, +] +`; + +exports[`Switch ePILTs against PILTS on Peregrine > receiver Peregrine::messageQueue::[Processed] 1`] = ` +[ + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": 1000, + }, + "success": true, + "weightUsed": { + "proofSize": 0, + "refTime": 1000000000, + }, + }, + "method": "Processed", + "section": "messageQueue", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchKiltAgainstEkilt.test.ts.snap b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchKiltAgainstEkilt.test.ts.snap new file mode 100644 index 000000000..4fd48a532 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/switchKiltAgainstEkilt.test.ts.snap @@ -0,0 +1,55 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Switch PILTs against ePILTS on AssetHub > sender Peregrine::assetSwitchPool1::[LocalToRemoteSwitchExecuted] 1`] = ` +[ + { + "data": { + "amount": 50000000000000000, + "from": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "to": { + "V4": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + }, + "method": "LocalToRemoteSwitchExecuted", + "section": "assetSwitchPool1", + }, +] +`; + +exports[`Switch PILTs against ePILTS on AssetHub > sender Peregrine::balances::[Transfer] 1`] = ` +[ + { + "data": { + "amount": 50000000000000000, + "from": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + "to": "4nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS", + }, + "method": "Transfer", + "section": "balances", + }, +] +`; + +exports[`Switch PILTs against ePILTS on AssetHub > sender Peregrine::xcmpQueue::[XcmpMessageSent] 1`] = ` +[ + { + "data": { + "messageHash": "(hash)", + }, + "method": "XcmpMessageSent", + "section": "xcmpQueue", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/trappedAssets.test.ts.snap b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/trappedAssets.test.ts.snap new file mode 100644 index 000000000..37b38d58b --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/__snapshots__/trappedAssets.test.ts.snap @@ -0,0 +1,189 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Trapped assets > receiver Peregrine::assetSwitchPool1::[RemoteToLocalSwitchExecuted] 1`] = ` +[ + { + "data": { + "amount": 1000000000000000, + "to": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + }, + "method": "RemoteToLocalSwitchExecuted", + "section": "assetSwitchPool1", + }, +] +`; + +exports[`Trapped assets > receiver Peregrine::messageQueue::[Processed] 1`] = ` +[ + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": 1000, + }, + "success": false, + "weightUsed": { + "proofSize": 0, + "refTime": 600000000, + }, + }, + "method": "Processed", + "section": "messageQueue", + }, +] +`; + +exports[`Trapped assets > receiver Peregrine::messageQueue::[Processed] 2`] = ` +[ + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": 1000, + }, + "success": true, + "weightUsed": { + "proofSize": 0, + "refTime": 400000000, + }, + }, + "method": "Processed", + "section": "messageQueue", + }, + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": 1000, + }, + "success": true, + "weightUsed": { + "proofSize": 0, + "refTime": 1000000000, + }, + }, + "method": "Processed", + "section": "messageQueue", + }, +] +`; + +exports[`Trapped assets > receiver Peregrine::polkadotXcm::[AssetsClaimed] 1`] = ` +[ + { + "data": { + "cost": [], + "destination": { + "interior": { + "X1": [ + { + "Parachain": 1000, + }, + ], + }, + "parents": 1, + }, + "messageId": "(hash)", + }, + "method": "VersionNotifyStarted", + "section": "polkadotXcm", + }, + { + "data": { + "assets": { + "V4": [ + { + "fun": { + "Fungible": 1000000000000000, + }, + "id": { + "interior": { + "X2": [ + { + "GlobalConsensus": { + "Ethereum": { + "chainId": "(rounded 11000000)", + }, + }, + }, + { + "AccountKey20": { + "key": "0x06012c8cf97bead5deae237070f9587f8e7a266d", + "network": null, + }, + }, + ], + }, + "parents": 2, + }, + }, + ], + }, + "hash_": "(hash)", + "origin": { + "interior": { + "X1": [ + { + "Parachain": 1000, + }, + ], + }, + "parents": 1, + }, + }, + "method": "AssetsClaimed", + "section": "polkadotXcm", + }, +] +`; + +exports[`Trapped assets > receiver Peregrine::polkadotXcm::[AssetsTrapped] 1`] = ` +[ + { + "data": { + "assets": { + "V4": [ + { + "fun": { + "Fungible": 1000000000000000, + }, + "id": { + "interior": { + "X2": [ + { + "GlobalConsensus": { + "Ethereum": { + "chainId": "(rounded 11000000)", + }, + }, + }, + { + "AccountKey20": { + "key": "0x06012c8cf97bead5deae237070f9587f8e7a266d", + "network": null, + }, + }, + ], + }, + "parents": 2, + }, + }, + ], + }, + "hash_": "(hash)", + "origin": { + "interior": { + "X1": [ + { + "Parachain": 1000, + }, + ], + }, + "parents": 1, + }, + }, + "method": "AssetsTrapped", + "section": "polkadotXcm", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/switchPallet/fullFlowSwitch.test.ts b/integration-tests/chopsticks/src/tests/switchPallet/fullFlowSwitch.test.ts new file mode 100644 index 000000000..2792fc925 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/fullFlowSwitch.test.ts @@ -0,0 +1,181 @@ +import { test } from 'vitest' + +import * as PeregrineConfig from '../../network/peregrine.js' +import * as AssetHubConfig from '../../network/assetHub.js' +import { + KILT, + ROC, + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysCharlie, +} from '../../utils.js' +import { + peregrineContext, + getFreeBalancePeregrine, + getFreeRocPeregrine, + getFreeEkiltAssetHub, + assethubContext, +} from '../index.js' +import { + checkBalance, + createBlock, + setStorage, + hexAddress, + checkBalanceInRange, + getXcmMessageV4ToSendEkilt, + checkSwitchPalletInvariant, + checkBalanceMovementIncomingSwitch, +} from '../utils.js' +import { getAccountLocationV4, getRelayNativeAssetIdLocationV4, getSiblingLocationV4 } from '../../network/utils.js' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +/** + * Full e2e flow between Peregrine and AssetHub. More checks are provided in individual test cases. + * + * 1. Send ROCs from AssetHub to Peregrine + * 2. Switch KILTs on Peregrine + * 3. Send eKILTs back to AssetHub + * 4. Send ROCs back to AssetHub + */ +test('Full e2e tests', async ({ expect }) => { + const { checkEvents } = withExpect(expect) + + // 10 % of relay tokens are used as fees + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, []), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + const switchParameters = getAssetSwitchParameters() + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [keysAlice.address, PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + // 1. send ROCs 2 Peregrine + + const peregrineDestination = getSiblingLocationV4(PeregrineConfig.paraId) + const beneficiary = getAccountLocationV4(hexAddress(keysAlice.address)) + const rocAsset = { V4: [getRelayNativeAssetIdLocationV4((ROC * BigInt(2)).toString())] } + + const signedTx1 = assethubContext.api.tx.polkadotXcm + .limitedReserveTransferAssets(peregrineDestination, beneficiary, rocAsset, 0, 'Unlimited') + .signAsync(keysAlice) + + // Send the transaction and create a block0 + await sendTransaction(signedTx1) + await createBlock(assethubContext) + + // process msg. + await createBlock(peregrineContext) + // Alice should have some Rocs on Peregrine + const aliceRocBalance = await getFreeRocPeregrine(keysAlice.address) + expect(aliceRocBalance).toBeGreaterThan(BigInt(0)) + + await checkSwitchPalletInvariant(expect) + + // 2. switch KILTs + const balanceToTransfer = initialBalanceKILT / BigInt(2) + const signedTx2 = peregrineContext.api.tx.assetSwitchPool1 + .switch(balanceToTransfer.toString(), beneficiary) + .signAsync(keysAlice) + + const events1 = await sendTransaction(signedTx2) + + await createBlock(peregrineContext) + await checkEvents(events1, 'assetSwitchPool1').toMatchSnapshot( + 'receiver Peregrine::assetSwitchPool1::[LocalToRemoteSwitchExecuted]' + ) + + await createBlock(assethubContext) + await checkBalance(getFreeEkiltAssetHub, keysAlice.address, expect, balanceToTransfer) + + await checkSwitchPalletInvariant(expect) + + // 3. send eKILTs back + const dest = getSiblingLocationV4(PeregrineConfig.paraId) + const remoteFeeId = { V4: AssetHubConfig.eKiltLocation } + const funds = { + V4: [ + { + id: AssetHubConfig.eKiltLocation, + fun: { Fungible: balanceToTransfer }, + }, + ], + } + + const signedTx3 = assethubContext.api.tx.polkadotXcm + .transferAssetsUsingTypeAndThen( + dest, + funds, + 'LocalReserve', + remoteFeeId, + 'LocalReserve', + getXcmMessageV4ToSendEkilt(keysAlice.address), + 'Unlimited' + ) + .signAsync(keysAlice) + + const events2 = await sendTransaction(signedTx3) + + await createBlock(assethubContext) + await checkBalance(getFreeEkiltAssetHub, keysAlice.address, expect, BigInt(0)) + // assets should move from Sovereign account to user. + await checkEvents(events2, { section: 'foreignAssets', method: 'Transferred' }).toMatchSnapshot( + 'sender AssetHub::foreignAssets::[Transferred]' + ) + + await createBlock(peregrineContext) + await checkBalanceInRange(getFreeBalancePeregrine, keysAlice.address, expect, [ + BigInt(74) * KILT, + BigInt(75) * KILT, + ]) + + await checkSwitchPalletInvariant(expect) + await checkBalanceMovementIncomingSwitch(balanceToTransfer, expect, keysAlice.address) + + // 4. send ROCs back + + const assetHubDestination = getSiblingLocationV4(AssetHubConfig.paraId) + const assets = { V4: [getRelayNativeAssetIdLocationV4(ROC.toString())] } + + const signedTx4 = peregrineContext.api.tx.polkadotXcm + .transferAssets(assetHubDestination, beneficiary, assets, 0, 'Unlimited') + .signAsync(keysAlice) + + const events3 = await sendTransaction(signedTx4) + await createBlock(peregrineContext) + + // The xcm message should be send to AH and the funds should be burned from user. + await checkEvents(events3, 'fungibles').toMatchSnapshot('sender Peregrine::fungibles::[Burned]') + + expect(await getFreeRocPeregrine(keysAlice.address)).toBe(BigInt(899999965317)) + + // Process the message on AH + await createBlock(assethubContext) + + await checkSwitchPalletInvariant(expect) +}, 20_000) diff --git a/integration-tests/chopsticks/src/tests/switchPallet/pauseSwitch.test.ts b/integration-tests/chopsticks/src/tests/switchPallet/pauseSwitch.test.ts new file mode 100644 index 000000000..433bbfe6f --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/pauseSwitch.test.ts @@ -0,0 +1,399 @@ +import { test } from 'vitest' + +import * as PeregrineConfig from '../../network/peregrine.js' +import * as AssetHubConfig from '../../network/assetHub.js' +import { + KILT, + ROC, + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysCharlie, +} from '../../utils.js' +import { peregrineContext, getFreeRocPeregrine, getFreeEkiltAssetHub, assethubContext } from '../index.js' +import { + checkBalance, + createBlock, + setStorage, + hexAddress, + getXcmMessageV4ToSendEkilt, + checkSwitchPalletInvariant, +} from '../utils.js' +import { getAccountLocationV4, getRelayNativeAssetIdLocationV4, getSiblingLocationV4 } from '../../network/utils.js' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +/** + * These test cases should primarily check the behavior of the switch pair when it is paused. + * Similar to the full end-to-end tests, but after each step, the switch pair is paused. + */ + +// Send ROCs while switch is paused +test('Send ROCs while switch paused', async ({ expect }) => { + const { checkSystemEvents } = withExpect(expect) + + const switchParameters = getAssetSwitchParameters() + + // 10 % of relay tokens are used as fees + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair( + switchParameters, + remoteAssetId, + remoteXcmFeeId, + remoteReserveLocation, + PeregrineConfig.initialPoolAccountId, + 'Paused' + ) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [keysAlice.address, PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + + const peregrineDestination = getSiblingLocationV4(PeregrineConfig.paraId) + const beneficiary1 = getAccountLocationV4(hexAddress(keysAlice.address)) + const rocAsset = { V4: [getRelayNativeAssetIdLocationV4(ROC.toString())] } + + const signedTx1 = assethubContext.api.tx.polkadotXcm + .limitedReserveTransferAssets(peregrineDestination, beneficiary1, rocAsset, 0, 'Unlimited') + .signAsync(keysAlice) + + await sendTransaction(signedTx1) + await createBlock(assethubContext) + + // ... But it should fail on Peregrine + await createBlock(peregrineContext) + const aliceRocBalance = await getFreeRocPeregrine(keysAlice.address) + + await checkSystemEvents(peregrineContext, { section: 'messageQueue', method: 'Processed' }).toMatchSnapshot( + 'receiver Peregrine::messageQueue::[Processed]' + ) + + await checkSystemEvents(peregrineContext, { section: 'polkadotXcm', method: 'AssetsTrapped' }).toMatchSnapshot( + 'receiver Peregrine::polkadotXcm::[AssetsTrapped]' + ) + + expect(aliceRocBalance).toBe(BigInt(0)) + + await checkSwitchPalletInvariant(expect) +}, 30_000) + +/** + * 1. Send Rocs + * 2. pause switch + * 3. switch KILTs + */ +test('Switch PILTs against ePILTs while paused', async ({ expect }) => { + const switchParameters = getAssetSwitchParameters() + + // 10 % of relay tokens are used as fees + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, []), + ...PeregrineConfig.setSafeXcmVersion4(), + ...PeregrineConfig.setSudoKey(keysAlice.address), + }) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [keysAlice.address, PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + + // 1. send ROCs 2 Peregrine + const peregrineDestination = getSiblingLocationV4(PeregrineConfig.paraId) + const beneficiary1 = getAccountLocationV4(hexAddress(keysAlice.address)) + const rocAsset = { V4: [getRelayNativeAssetIdLocationV4(ROC.toString())] } + + const signedTx1 = assethubContext.api.tx.polkadotXcm + .limitedReserveTransferAssets(peregrineDestination, beneficiary1, rocAsset, 0, 'Unlimited') + .signAsync(keysAlice) + + await sendTransaction(signedTx1) + await createBlock(assethubContext) + + // process msg. + await createBlock(peregrineContext) + const aliceRocBalance = await getFreeRocPeregrine(keysAlice.address) + expect(aliceRocBalance).toBeGreaterThan(BigInt(0)) + + // 2. Pause switch pair + await peregrineContext.api.tx.sudo + .sudo(peregrineContext.api.tx.assetSwitchPool1.pauseSwitchPair()) + .signAndSend(keysAlice) + await createBlock(peregrineContext) + + // 3. switch KILTs + const balanceToTransfer = initialBalanceKILT / BigInt(2) + + const beneficiary = getAccountLocationV4(hexAddress(keysAlice.address)) + + let section: string = '' + let errorName: string = '' + + // This should fail. + await peregrineContext.api.tx.assetSwitchPool1 + .switch(balanceToTransfer.toString(), beneficiary) + .signAndSend(keysAlice, ({ dispatchError }) => { + if (dispatchError) { + const decoded = peregrineContext.api.registry.findMetaError(dispatchError.asModule) + section = decoded.section + errorName = decoded.name + } + }) + + await createBlock(peregrineContext) + + expect(section).toBe('assetSwitchPool1') + expect(errorName).toBe('SwitchPairNotEnabled') + + await checkSwitchPalletInvariant(expect) +}, 30_000) + +/** + * 1. Send Rocs + * 2. switch KILTs + * 3. pause switch + * 4. send eKILTs back + */ +test('Switch ePILTs against PILTs while paused', async ({ expect }) => { + const { checkEvents, checkSystemEvents } = withExpect(expect) + + const switchParameters = getAssetSwitchParameters() + + // 10 % of relay tokens are used as fees + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, []), + ...PeregrineConfig.setSafeXcmVersion4(), + ...PeregrineConfig.setSudoKey(keysAlice.address), + }) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [keysAlice.address, PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + + // 1. send ROCs 2 Peregrine + const peregrineDestination = getSiblingLocationV4(PeregrineConfig.paraId) + const beneficiary1 = getAccountLocationV4(hexAddress(keysAlice.address)) + const rocAsset = { V4: [getRelayNativeAssetIdLocationV4(ROC.toString())] } + + const signedTx1 = assethubContext.api.tx.polkadotXcm + .limitedReserveTransferAssets(peregrineDestination, beneficiary1, rocAsset, 0, 'Unlimited') + .signAsync(keysAlice) + await sendTransaction(signedTx1) + + // send msg + await createBlock(assethubContext) + // process msg. + await createBlock(peregrineContext) + + const aliceRocBalance = await getFreeRocPeregrine(keysAlice.address) + expect(aliceRocBalance).toBeGreaterThan(BigInt(0)) + + // 2. switch KILTs + const balanceToTransfer = initialBalanceKILT / BigInt(2) + + const beneficiary = getAccountLocationV4(hexAddress(keysAlice.address)) + + const signedTx2 = peregrineContext.api.tx.assetSwitchPool1 + .switch(balanceToTransfer.toString(), beneficiary) + .signAsync(keysAlice) + + const events2 = await sendTransaction(signedTx2) + + await createBlock(peregrineContext) + + await checkEvents(events2, 'assetSwitchPool1').toMatchSnapshot( + 'sender Peregrine::assetSwitchPool1::[LocalToRemoteSwitchExecuted]' + ) + await createBlock(assethubContext) + + // only check here, if alice received the funds + const balanceAliceEkilt = await getFreeEkiltAssetHub(keysAlice.address) + expect(balanceAliceEkilt).toBe(balanceToTransfer) + + // 3. Pause swap pairs + await peregrineContext.api.tx.sudo + .sudo(peregrineContext.api.tx.assetSwitchPool1.pauseSwitchPair()) + .signAndSend(keysAlice) + await createBlock(peregrineContext) + + // 4. send eKILTs back + const balanceToTransferBack = balanceToTransfer / BigInt(2) + const dest = getSiblingLocationV4(PeregrineConfig.paraId) + const remoteFeeId = { V4: AssetHubConfig.eKiltLocation } + const funds = { + V4: [ + { + id: AssetHubConfig.eKiltLocation, + fun: { Fungible: balanceToTransferBack.toString() }, + }, + ], + } + + const signedTx3 = assethubContext.api.tx.polkadotXcm + .transferAssetsUsingTypeAndThen( + dest, + funds, + 'LocalReserve', + remoteFeeId, + 'LocalReserve', + getXcmMessageV4ToSendEkilt(keysAlice.address), + 'Unlimited' + ) + .signAsync(keysAlice) + + await sendTransaction(signedTx3) + + await createBlock(assethubContext) + + // Tx should not fail on AH. + await checkBalance(getFreeEkiltAssetHub, keysAlice.address, expect, KILT * BigInt(25)) + + // prcess msg + await createBlock(peregrineContext) + + // ... but MSG execution should fail on Peregrine + await checkSystemEvents(peregrineContext, { section: 'messageQueue', method: 'Processed' }).toMatchSnapshot( + 'receiver Peregrine::messageQueue::[Processed]' + ) + await checkSystemEvents(peregrineContext, { section: 'polkadotXcm', method: 'AssetsTrapped' }).toMatchSnapshot( + 'receiver Peregrine::polkadotXcm::[AssetsTrapped]' + ) + + // The msg will not be processed. Therefore, some assets are not moved. We can not do strict checks here. + await checkSwitchPalletInvariant(expect, balanceToTransferBack) +}, 30_000) + +test('Withdraw ROCs while switch is paused', async ({ expect }) => { + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, [keysAlice.address]), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + const switchParameters = getAssetSwitchParameters() + // 10 % of relay tokens are used as fees + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair( + switchParameters, + remoteAssetId, + remoteXcmFeeId, + remoteReserveLocation, + PeregrineConfig.initialPoolAccountId, + 'Paused' + ) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + + let section: string = '' + let errorName: string = '' + + const assetHubDestination = getSiblingLocationV4(AssetHubConfig.paraId) + const assets = { V4: [getRelayNativeAssetIdLocationV4(ROC.toString())] } + const beneficiary = getAccountLocationV4(hexAddress(keysAlice.address)) + + await peregrineContext.api.tx.polkadotXcm + .transferAssets(assetHubDestination, beneficiary, assets, 0, 'Unlimited') + .signAndSend(keysAlice, ({ dispatchError }) => { + if (dispatchError) { + const decoded = peregrineContext.api.registry.findMetaError(dispatchError.asModule) + section = decoded.section + errorName = decoded.name + } + }) + + await createBlock(peregrineContext) + + expect(section).toBe('polkadotXcm') + expect(errorName).toBe('LocalExecutionIncomplete') + + await checkSwitchPalletInvariant(expect) +}, 30_000) diff --git a/integration-tests/chopsticks/src/tests/switchPallet/relayToken.test.ts b/integration-tests/chopsticks/src/tests/switchPallet/relayToken.test.ts new file mode 100644 index 000000000..e4c64cdde --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/relayToken.test.ts @@ -0,0 +1,172 @@ +import { test } from 'vitest' + +import * as PeregrineConfig from '../../network/peregrine.js' +import * as BasiliskConfig from '../../network/basilisk.js' +import * as AssetHubConfig from '../../network/assetHub.js' +import { + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysBob, + keysCharlie, + ROC, +} from '../../utils.js' +import { + peregrineContext, + getFreeBalancePeregrine, + getFreeRocPeregrine, + basiliskContext, + rococoContext, + assethubContext, +} from '../index.js' +import { + checkBalance, + createBlock, + setStorage, + hexAddress, + checkBalanceInRange, + checkSwitchPalletInvariant, +} from '../utils.js' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' +import { getSiblingLocationV4 } from '../../network/utils.js' + +test('User transfers all of his dots', async ({ expect }) => { + const { checkEvents } = withExpect(expect) + + // Assign alice some KILTs and ROCs + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address, keysBob.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, [keysAlice.address]), + }) + + const signedTx = peregrineContext.api.tx.fungibles + .transfer(PeregrineConfig.ROC_LOCATION, keysBob.address, initialBalanceROC) + .signAsync(keysAlice) + + const events = await sendTransaction(signedTx) + + await createBlock(peregrineContext) + + checkEvents(events, { section: 'fungibles', method: 'Transferred' }).toMatchSnapshot( + `local Peregrine::fungibles::[Transferred] asset ${JSON.stringify(PeregrineConfig.ROC_LOCATION)}` + ) + + await checkBalance(getFreeRocPeregrine, keysAlice.address, expect, BigInt(0)) + + await checkBalance(getFreeRocPeregrine, keysBob.address, expect, initialBalanceROC) + + await checkBalanceInRange(getFreeBalancePeregrine, keysAlice.address, expect, [ + BigInt('99999800999995545'), + initialBalanceKILT, + ]) +}, 20_000) + +test('User gets dusted with ROCs', async ({ expect }) => { + const { checkEvents } = withExpect(expect) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, [keysAlice.address]), + }) + + const balanceToTransfer = (initialBalanceKILT * BigInt(999998)) / BigInt(1000000) + + const signedTx = peregrineContext.api.tx.balances + .transferAllowDeath(keysBob.address, balanceToTransfer) + .signAsync(keysAlice) + + const events = await sendTransaction(signedTx) + + await createBlock(peregrineContext) + + checkEvents(events, { section: 'balances', method: 'Transfer' }).toMatchSnapshot( + 'local Peregrine::balances::[Transfer] native asset' + ) + // User should get dusted by this operation + checkEvents(events, { section: 'balances', method: 'DustLost' }).toMatchSnapshot( + 'local balances::fungibles::[DustLost]' + ) + // he should keep his rocs + await checkBalance(getFreeRocPeregrine, keysAlice.address, expect, initialBalanceROC) +}, 20_000) + +test('Send DOTs from basilisk 2 Peregrine', async ({ expect }) => { + const { checkSystemEvents } = withExpect(expect) + + const switchParameters = getAssetSwitchParameters() + + // 10 % of relay tokens are used as fees + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, []), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + + await setStorage(basiliskContext, { + ...BasiliskConfig.assignNativeTokensToAccounts([keysAlice.address]), + ...BasiliskConfig.assignRocTokensToAccounts([keysAlice.address], initialBalanceROC), + }) + + const balanceToTransfer = initialBalanceROC / BigInt(2) + + const beneficiary = { + V4: { + parents: 1, + interior: { + X2: [ + { Parachain: PeregrineConfig.paraId }, + { + AccountId32: { + id: hexAddress(keysAlice.address), + }, + }, + ], + }, + }, + } + + const signedTx = basiliskContext.api.tx.xTokens + .transfer(BasiliskConfig.dotTokenId, balanceToTransfer, beneficiary, 'Unlimited') + .signAsync(keysAlice) + + await sendTransaction(signedTx) + await createBlock(basiliskContext) + + // FWD the message + await createBlock(rococoContext) + + // Process the message + await createBlock(peregrineContext) + // Barrier blocked execution. No event will be emitted. + await checkSystemEvents(peregrineContext, 'messageQueue').toMatchSnapshot( + 'receiver Peregrine::messageQueue::[ProcessingFailed]' + ) + + // Alice should still have no rocs. + await checkBalance(getFreeRocPeregrine, keysAlice.address, expect, BigInt(0)) + + await checkSwitchPalletInvariant(expect) +}, 20_000) diff --git a/integration-tests/chopsticks/src/tests/switchPallet/switchConfig.test.ts b/integration-tests/chopsticks/src/tests/switchPallet/switchConfig.test.ts new file mode 100644 index 000000000..c042bf1c8 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/switchConfig.test.ts @@ -0,0 +1,228 @@ +import { test } from 'vitest' + +import * as PeregrineConfig from '../../network/peregrine.js' +import * as AssetHubConfig from '../../network/assetHub.js' +import * as RococoConfig from '../../network/rococo.js' +import { + KILT, + ROC, + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysCharlie, +} from '../../utils.js' +import { peregrineContext, getFreeEkiltAssetHub, assethubContext, getFreeRocAssetHub, rococoContext } from '../index.js' +import { + checkSwitchPalletInvariant, + checkBalance, + createBlock, + setStorage, + hexAddress, + getXcmMessageV4ToSendEkilt, +} from '../utils.js' +import { getAccountLocationV4, getChildLocation, getSiblingLocationV4 } from '../../network/utils.js' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +test('Switch KILTs against EKILTs no enough DOTs on AH', async ({ expect }) => { + const { checkEvents } = withExpect(expect) + + const switchParameters = getAssetSwitchParameters() + + // 10 % of relay tokens are used as fees + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, [keysAlice.address]), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + + const balanceToTransfer = initialBalanceKILT / BigInt(2) + + const beneficiary = getAccountLocationV4(hexAddress(keysAlice.address)) + + const signedTx = peregrineContext.api.tx.assetSwitchPool1 + .switch(balanceToTransfer.toString(), beneficiary) + .signAsync(keysAlice) + + const events = await sendTransaction(signedTx) + + await createBlock(peregrineContext) + + checkEvents(events, 'xcmpQueue').toMatchSnapshot('sender Peregrine::xcmpQueue::[XcmpMessageSent]') + checkEvents(events, 'assetSwitchPool1').toMatchSnapshot( + 'sender Peregrine::assetSwitchPool1::[LocalToRemoteSwitchExecuted]' + ) + checkEvents(events, { section: 'balances', method: 'Transfer' }).toMatchSnapshot( + 'sender Peregrine::balances::[Transfer]' + ) + + // process msg. We don't care about the events. We check only the funds. + await createBlock(assethubContext) + + await checkBalance(getFreeEkiltAssetHub, keysAlice.address, expect, BigInt(0)) + await checkBalance(getFreeRocAssetHub, keysAlice.address, expect, BigInt(0)) + + await checkSwitchPalletInvariant(expect, balanceToTransfer) +}, 20_000) + +test('Send eKILT while switch Pair does not exist', async ({ expect }) => { + const { checkSystemEvents } = withExpect(expect) + + const switchParameters = getAssetSwitchParameters(initialBalanceKILT * BigInt(1000)) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [keysAlice.address, PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + ...AssetHubConfig.assignForeignAssetToAccounts([[keysAlice.address, switchParameters.circulatingSupply]]), + }) + + const dest = getSiblingLocationV4(PeregrineConfig.paraId) + const remoteFeeId = { V4: AssetHubConfig.eKiltLocation } + + const funds = { + V4: [ + { + id: AssetHubConfig.eKiltLocation, + fun: { Fungible: KILT }, + }, + ], + } + + const signedTx = assethubContext.api.tx.polkadotXcm + .transferAssetsUsingTypeAndThen( + dest, + funds, + 'LocalReserve', + remoteFeeId, + 'LocalReserve', + getXcmMessageV4ToSendEkilt(keysAlice.address), + 'Unlimited' + ) + .signAsync(keysAlice) + + await sendTransaction(signedTx) + // send msg + await createBlock(assethubContext) + + // Will fail on the receiver side + await createBlock(peregrineContext) + await checkSystemEvents(peregrineContext, 'messageQueue').toMatchSnapshot( + 'receiver Peregrine::messageQueue::[Processed]' + ) +}, 20_000) + +test('Send eKILT from other reserve location', async ({ expect }) => { + const { checkSystemEvents } = withExpect(expect) + + const switchParameters = getAssetSwitchParameters() + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(rococoContext, { + ...RococoConfig.setSudoKey(keysAlice.address), + ...RococoConfig.assignNativeTokensToAccounts([keysAlice.address]), + }) + + await setStorage(assethubContext, { + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + const dest = { V3: getChildLocation(PeregrineConfig.paraId) } + + const xcmMessage = { + V3: [ + { + ReserveAssetDeposited: [ + { + id: { Concrete: AssetHubConfig.eKiltLocation }, + fun: { Fungible: initialBalanceKILT }, + }, + ], + }, + 'ClearOrigin', + { + BuyExecution: { + fees: { + id: { Concrete: AssetHubConfig.eKiltLocation }, + fun: { Fungible: initialBalanceKILT }, + }, + weightLimit: 'Unlimited', + }, + }, + { + DepositAsset: { + assets: { Wild: 'All' }, + beneficiary: { + parents: 0, + interior: { + X1: { + AccountId32: { + id: hexAddress(keysAlice.address), + }, + }, + }, + }, + }, + }, + ], + } + + const innerTx = rococoContext.api.tx.xcmPallet.send(dest, xcmMessage) + + const tx = rococoContext.api.tx.sudo.sudo(innerTx).signAsync(keysAlice) + + // send msg + await sendTransaction(tx) + await createBlock(rococoContext) + + await createBlock(peregrineContext) + // We expect the UntrustedReserveLocation error which results in failing the msg. The error will NOT emitted as an event. + await checkSystemEvents(peregrineContext, 'messageQueue').toMatchSnapshot( + 'receiver Peregrine::messageQueue::[Processed]' + ) + + await checkSwitchPalletInvariant(expect) +}, 20_000) diff --git a/integration-tests/chopsticks/src/tests/switchPallet/switchEkiltAgainstKilt.test.ts b/integration-tests/chopsticks/src/tests/switchPallet/switchEkiltAgainstKilt.test.ts new file mode 100644 index 000000000..4c3567659 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/switchEkiltAgainstKilt.test.ts @@ -0,0 +1,133 @@ +import { test } from 'vitest' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +import * as PeregrineConfig from '../../network/peregrine.js' +import * as AssetHubConfig from '../../network/assetHub.js' +import { KILT, ROC, getAssetSwitchParameters, initialBalanceROC, keysAlice, keysCharlie } from '../../utils.js' +import { + peregrineContext, + getFreeBalancePeregrine, + getFreeRocPeregrine, + getFreeEkiltAssetHub, + assethubContext, + getRemoteLockedSupply, +} from '../index.js' +import { + checkSwitchPalletInvariant, + checkBalance, + createBlock, + setStorage, + getXcmMessageV4ToSendEkilt, + checkBalanceInRange, + checkBalanceMovementIncomingSwitch, +} from '../utils.js' +import { getSiblingLocationV4 } from '../../network/utils.js' + +test('Switch ePILTs against PILTS on Peregrine', async ({ expect }) => { + const { checkSystemEvents } = withExpect(expect) + + const switchParameters = getAssetSwitchParameters() + // alice has the whole circulating supply. + const fundsAlice = switchParameters.circulatingSupply + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, [keysAlice.address], initialBalanceROC), + ...PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [keysAlice.address, PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + [keysAlice.address, fundsAlice], + ]) + ) + + // check initial balance of Alice on Spiritnet. Alice should have 0 KILT + await checkBalance(getFreeBalancePeregrine, keysAlice.address, expect, BigInt(0)) + await checkBalance(getFreeRocPeregrine, keysAlice.address, expect, initialBalanceROC) + await checkBalance(getFreeEkiltAssetHub, keysAlice.address, expect, switchParameters.circulatingSupply) + + const initialBalancePoolAccount = await getFreeBalancePeregrine(PeregrineConfig.initialPoolAccountId) + const initialBalanceSovereignAccount = await getFreeEkiltAssetHub(PeregrineConfig.sovereignAccountAsSibling) + const initialRemoteLockedSupply = await getRemoteLockedSupply() + + // 50 PILTS + const balanceToTransfer = BigInt('50000000000000000') + + const dest = getSiblingLocationV4(PeregrineConfig.paraId) + + const remoteFeeId = { V4: AssetHubConfig.eKiltLocation } + + const funds = { + V4: [ + { + id: AssetHubConfig.eKiltLocation, + fun: { Fungible: balanceToTransfer.toString() }, + }, + ], + } + + const signedTx = assethubContext.api.tx.polkadotXcm.transferAssetsUsingTypeAndThen( + dest, + funds, + 'LocalReserve', + remoteFeeId, + 'LocalReserve', + getXcmMessageV4ToSendEkilt(keysAlice.address), + 'Unlimited' + ) + + // send msg + await sendTransaction(signedTx.signAsync(keysAlice)) + await createBlock(assethubContext) + + // check balance. Alice should have 50 ePILTs less + const freeBalanceAlice = await getFreeEkiltAssetHub(keysAlice.address) + expect(freeBalanceAlice).toBe(fundsAlice - balanceToTransfer) + + // the sovereign account should have 50 more PILTs + const balanceSovereignAccountAfterTx = await getFreeEkiltAssetHub(PeregrineConfig.sovereignAccountAsSibling) + expect(balanceSovereignAccountAfterTx).eq(initialBalanceSovereignAccount + balanceToTransfer) + + await createBlock(peregrineContext) + + checkSystemEvents(peregrineContext, 'messageQueue').toMatchSnapshot('receiver Peregrine::messageQueue::[Processed]') + checkSystemEvents(peregrineContext, 'assetSwitchPool1').toMatchSnapshot( + 'receiver Peregrine::assetSwitchPool1::[RemoteToLocalSwitchExecuted]' + ) + checkSystemEvents(peregrineContext, { section: 'balances', method: 'Transfer' }).toMatchSnapshot( + 'receiver Peregrine::balances::[Transfer]' + ) + + // Alice should have some coins now. Calculating the exact amount is not easy. Since fees are taken by xcm. + checkBalanceInRange(getFreeBalancePeregrine, keysAlice.address, expect, [ + balanceToTransfer - KILT, + balanceToTransfer, + ]) + + // Pool account should have less locked PILTs + const freeBalancePoolAccount = await getFreeBalancePeregrine(PeregrineConfig.initialPoolAccountId) + expect(initialBalancePoolAccount - balanceToTransfer).toBe(freeBalancePoolAccount) + + // remote locked supply should have increased by the amount of the transferred PILTs + const remoteLockedSupply = await getRemoteLockedSupply() + expect(remoteLockedSupply).toBe(initialRemoteLockedSupply + balanceToTransfer) + + await checkSwitchPalletInvariant(expect) + await checkBalanceMovementIncomingSwitch(balanceToTransfer, expect, keysAlice.address) +}, 20_000) diff --git a/integration-tests/chopsticks/src/tests/switchPallet/switchKiltAgainstEkilt.test.ts b/integration-tests/chopsticks/src/tests/switchPallet/switchKiltAgainstEkilt.test.ts new file mode 100644 index 000000000..7ead64816 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/switchKiltAgainstEkilt.test.ts @@ -0,0 +1,129 @@ +import { test } from 'vitest' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +import * as PeregrineConfig from '../../network/peregrine.js' +import * as AssetHubConfig from '../../network/assetHub.js' +import { + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysCharlie, + ROC, +} from '../../utils.js' +import { + peregrineContext, + getFreeBalancePeregrine, + getFreeRocPeregrine, + getFreeEkiltAssetHub, + assethubContext, + getFreeRocAssetHub, + getRemoteLockedSupply, +} from '../index.js' +import { checkSwitchPalletInvariant, checkBalance, createBlock, setStorage, hexAddress } from '../utils.js' +import { getAccountLocationV4, getSiblingLocationV4 } from '../../network/utils.js' + +test('Switch PILTs against ePILTS on AssetHub', async ({ expect }) => { + const { checkEvents } = withExpect(expect) + + const switchParameters = getAssetSwitchParameters() + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + // Assign alice some KILT and ROC tokens + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, [keysAlice.address], initialBalanceROC), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [keysAlice.address, PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchParameters.sovereignSupply], + ]) + ) + + // check initial balance of Alice on Spiritnet + await checkBalance(getFreeBalancePeregrine, keysAlice.address, expect, initialBalanceKILT) + await checkBalance(getFreeRocPeregrine, keysAlice.address, expect, initialBalanceROC) + + // Alice should have NO eKILT on AH + await checkBalance(getFreeEkiltAssetHub, keysAlice.address, expect, BigInt(0)) + + // initial balance of the pool account and sovereign account + const initialBalancePoolAccount = await getFreeBalancePeregrine(PeregrineConfig.initialPoolAccountId) + const initialBalanceSovereignAccount = await getFreeEkiltAssetHub(PeregrineConfig.sovereignAccountAsSibling) + const initialBalanceRocSovereignAccount = await getFreeRocAssetHub(PeregrineConfig.sovereignAccountAsSibling) + const initialRemoteLockedSupply = await getRemoteLockedSupply() + + // 50 PILTS + const balanceToTransfer = BigInt('50000000000000000') + + const beneficiary = getAccountLocationV4(hexAddress(keysAlice.address)) + + const signedTx = peregrineContext.api.tx.assetSwitchPool1.switch(balanceToTransfer.toString(), beneficiary) + const fees = (await signedTx.paymentInfo(keysAlice)).partialFee.toBigInt() + + const events = await sendTransaction(signedTx.signAsync(keysAlice)) + + await createBlock(peregrineContext) + + await checkEvents(events, 'xcmpQueue').toMatchSnapshot('sender Peregrine::xcmpQueue::[XcmpMessageSent]') + await checkEvents(events, 'assetSwitchPool1').toMatchSnapshot( + 'sender Peregrine::assetSwitchPool1::[LocalToRemoteSwitchExecuted]' + ) + await checkEvents(events, { section: 'balances', method: 'Transfer' }).toMatchSnapshot( + 'sender Peregrine::balances::[Transfer]' + ) + + // check balance. Alice had 50 PILts + const freeBalanceAlice = await getFreeBalancePeregrine(keysAlice.address) + expect(freeBalanceAlice).toBe(initialBalanceKILT - balanceToTransfer - fees) + + // check balance Alice. Some fees should have been paid with her rocs: + const freeRocBalanceAlice = await getFreeRocPeregrine(keysAlice.address) + expect(freeRocBalanceAlice).eq(initialBalanceROC - BigInt(PeregrineConfig.remoteFee)) + + // the Switch pool account should have 50 more PILTs + const balancePoolAccountAfterTx = await getFreeBalancePeregrine(PeregrineConfig.initialPoolAccountId) + expect(balancePoolAccountAfterTx).eq(initialBalancePoolAccount + balanceToTransfer) + + // process the msg + await createBlock(assethubContext) + + // alice should have the exact transferred amount of eKILT. Fees are paid by sovereign account + const freeBalanceAliceAssetHub = await getFreeEkiltAssetHub(keysAlice.address) + expect(freeBalanceAliceAssetHub).eq(balanceToTransfer) + + // sovereign account should have less eKILT by the amount of the transferred PILTs + const freeBalanceSovereignAccount = await getFreeEkiltAssetHub(PeregrineConfig.sovereignAccountAsSibling) + expect(initialBalanceSovereignAccount - balanceToTransfer).eq(freeBalanceSovereignAccount) + + // sovereign account should have paid the fees. Calculating the fees is not simple in context of XCM. + // We just check that the balance has decreased + const freeRocsSovereignAccount = await getFreeRocAssetHub(PeregrineConfig.sovereignAccountAsSibling) + expect(freeRocsSovereignAccount).toBeLessThan(initialBalanceRocSovereignAccount) + + // remote locked supply should have decreased by the amount of the transferred PILTs + const remoteLockedSupply = await getRemoteLockedSupply() + expect(remoteLockedSupply).eq(initialRemoteLockedSupply - balanceToTransfer) + + await checkSwitchPalletInvariant(expect) +}, 20_000) diff --git a/integration-tests/chopsticks/src/tests/switchPallet/trappedAssets.test.ts b/integration-tests/chopsticks/src/tests/switchPallet/trappedAssets.test.ts new file mode 100644 index 000000000..03bb70e87 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/switchPallet/trappedAssets.test.ts @@ -0,0 +1,209 @@ +import { test } from 'vitest' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +import * as PeregrineConfig from '../../network/peregrine.js' +import * as AssetHubConfig from '../../network/assetHub.js' +import * as RococoConfig from '../../network/rococo.js' +import { + KILT, + ROC, + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysCharlie, +} from '../../utils.js' +import { peregrineContext, assethubContext, rococoContext } from '../index.js' +import { + createBlock, + setStorage, + hexAddress, + getXcmMessageV4ToSendEkilt, + checkSwitchPalletInvariant, +} from '../utils.js' +import { getChildLocation, getSiblingLocationV4 } from '../../network/utils.js' + +/** + * 1. send eKILTs to peregrine while switch is paused + * 2. enable switch pair again + * 3. reclaim the assets + */ +test('Trapped assets', async ({ expect }) => { + const { checkSystemEvents } = withExpect(expect) + const switchPairParameters = getAssetSwitchParameters() + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, [ + keysAlice.address, + AssetHubConfig.sovereignAccountOnSiblingChains, + ]), + ...PeregrineConfig.setSafeXcmVersion4(), + ...PeregrineConfig.assignNativeTokensToAccounts( + [keysAlice.address, AssetHubConfig.sovereignAccountOnSiblingChains], + initialBalanceKILT + ), + ...PeregrineConfig.setSudoKey(keysAlice.address), + }) + + // pause switch pair + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair( + switchPairParameters, + remoteAssetId, + remoteXcmFeeId, + remoteReserveLocation, + PeregrineConfig.initialPoolAccountId, + 'Paused' + ) + ) + + await setStorage(rococoContext, { + ...RococoConfig.setSudoKey(keysAlice.address), + ...RococoConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceROC), + }) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [keysAlice.address, PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + ...AssetHubConfig.createForeignAsset(keysCharlie.address), + }) + + await setStorage( + assethubContext, + AssetHubConfig.assignForeignAssetToAccounts([ + [PeregrineConfig.sovereignAccountAsSibling, switchPairParameters.sovereignSupply], + [keysAlice.address, switchPairParameters.circulatingSupply], + ]) + ) + + // 1. send the coin and force a trap + const dest = getSiblingLocationV4(PeregrineConfig.paraId) + const remoteFeeId = { v4: AssetHubConfig.eKiltLocation } + + const funds = { + v4: [ + { + id: AssetHubConfig.eKiltLocation, + fun: { Fungible: KILT.toString() }, + }, + ], + } + + const signedTx = assethubContext.api.tx.polkadotXcm + .transferAssetsUsingTypeAndThen( + dest, + funds, + 'LocalReserve', + remoteFeeId, + 'LocalReserve', + getXcmMessageV4ToSendEkilt(keysAlice.address), + 'Unlimited' + ) + .signAsync(keysAlice) + + // send msg + await sendTransaction(signedTx) + await createBlock(assethubContext) + + // Process msg. Fails on receiver side. + await createBlock(peregrineContext) + await checkSystemEvents(peregrineContext, 'messageQueue').toMatchSnapshot( + 'receiver Peregrine::messageQueue::[Processed]' + ) + await checkSystemEvents(peregrineContext, 'polkadotXcm').toMatchSnapshot( + 'receiver Peregrine::polkadotXcm::[AssetsTrapped]' + ) + + // 2. enable switch pair again + await peregrineContext.api.tx.sudo + .sudo(peregrineContext.api.tx.assetSwitchPool1.resumeSwitchPair()) + .signAndSend(keysAlice) + + //3. reclaim msg + const reclaimMsg = [ + { WithdrawAsset: [{ id: { parents: 0, interior: 'Here' }, fun: { Fungible: KILT } }] }, + { + BuyExecution: { + weightLimit: 'Unlimited', + fees: { id: { parents: 0, interior: 'Here' }, fun: { Fungible: KILT } }, + }, + }, + { + ClaimAsset: { + // Specify xcm version 4 + ticket: { parents: 0, interior: { X1: [{ GeneralIndex: 4 }] } }, + assets: [ + { + id: AssetHubConfig.eKiltLocation, + fun: { Fungible: KILT.toString() }, + }, + ], + }, + }, + { + DepositAsset: { + assets: { Wild: 'All' }, + beneficiary: { + parents: 0, + interior: { + X1: [ + { + AccountId32: { + id: hexAddress(keysAlice.address), + }, + }, + ], + }, + }, + }, + }, + ] + + const peregrineDestination = getSiblingLocationV4(PeregrineConfig.paraId) + const transactExtrinsic = assethubContext.api.tx.polkadotXcm.send(peregrineDestination, { V4: reclaimMsg }) + const assetHubDestination = getChildLocation(AssetHubConfig.paraId) + + const transactMessage = [ + { UnpaidExecution: { weightLimit: 'Unlimited' } }, + { + Transact: { + originKind: 'SuperUser', + requireWeightAtMost: { refTime: '1000000000', proofSize: '65527' }, + call: { + encoded: transactExtrinsic.method.toHex(), + }, + }, + }, + ] + + const relayTx = rococoContext.api.tx.xcmPallet.send({ V3: assetHubDestination }, { V3: transactMessage }) + const reclaimTx = rococoContext.api.tx.sudo.sudo(relayTx).signAsync(keysAlice) + + // send msg + await sendTransaction(reclaimTx) + await createBlock(rococoContext) + + // forwards the msg. + await createBlock(assethubContext) + + // Assets should be reclaimed now. Check the events. + await createBlock(peregrineContext) + await checkSystemEvents(peregrineContext, 'messageQueue').toMatchSnapshot( + 'receiver Peregrine::messageQueue::[Processed]' + ) + await checkSystemEvents(peregrineContext, 'polkadotXcm').toMatchSnapshot( + 'receiver Peregrine::polkadotXcm::[AssetsClaimed]' + ) + await checkSystemEvents(peregrineContext, 'assetSwitchPool1').toMatchSnapshot( + 'receiver Peregrine::assetSwitchPool1::[RemoteToLocalSwitchExecuted]' + ) + + await checkSwitchPalletInvariant(expect) +}, 20_000) diff --git a/integration-tests/chopsticks/src/tests/utils.ts b/integration-tests/chopsticks/src/tests/utils.ts index 60cff25e9..b9fcc85ef 100644 --- a/integration-tests/chopsticks/src/tests/utils.ts +++ b/integration-tests/chopsticks/src/tests/utils.ts @@ -4,6 +4,14 @@ import { u8aToHex } from '@polkadot/util' import { decodeAddress } from '@polkadot/util-crypto' import { Config } from '../network/types.js' +import { + getCurrentBlockNumber, + getFreeBalancePeregrine, + getFreeBalancePeregrineAt, + getFreeEkiltAssetHub, + peregrineContext, +} from './index.js' +import * as PeregrineConfig from '../network/peregrine.js' /// Creates a new block for the given context export async function createBlock(context: Config) { @@ -45,3 +53,86 @@ export async function checkBalanceInRange( export function hexAddress(addr: string) { return u8aToHex(decodeAddress(addr)) } + +export function getXcmMessageV4ToSendEkilt(address: string) { + return { + V4: [ + { + DepositAsset: { + assets: { Wild: 'All' }, + beneficiary: { + parents: 0, + interior: { + X1: [ + { + AccountId32: { + id: hexAddress(address), + }, + }, + ], + }, + }, + }, + }, + ], + } +} + +// Delta represents the amount of trapped assets on the KILT side +export async function checkSwitchPalletInvariant(expect: ExpectStatic, deltaStoredSovereignSupply = BigInt(0)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const switchPairInfo: any = await peregrineContext.api.query.assetSwitchPool1.switchPair() + if (switchPairInfo.isNone) { + return + } + + // check pool account balance + const switchPoolAccount = switchPairInfo.unwrap().poolAccount + + const poolAccountBalance = await getFreeBalancePeregrine(switchPoolAccount) + + const sovereignEKiltSupply = await getFreeEkiltAssetHub(PeregrineConfig.sovereignAccountAsSibling) + + const remoteAssetSovereignTotalBalance = switchPairInfo.unwrap().remoteAssetSovereignTotalBalance.toBigInt() + const remoteAssetCirculatingSupply = switchPairInfo.unwrap().remoteAssetCirculatingSupply.toBigInt() + const remoteAssetTotalSupply = switchPairInfo.unwrap().remoteAssetTotalSupply.toBigInt() + + const lockedBalanceFromTotalAndCirculating = remoteAssetTotalSupply - remoteAssetCirculatingSupply + + // Check pool account has enough funds to cover the circulating supply + + expect(poolAccountBalance).toBe(remoteAssetCirculatingSupply) + expect(remoteAssetSovereignTotalBalance).toBe(lockedBalanceFromTotalAndCirculating) + expect(sovereignEKiltSupply).toBe(remoteAssetSovereignTotalBalance + deltaStoredSovereignSupply) +} + +export async function checkBalanceMovementIncomingSwitch( + transferredBalance: bigint, + expect: ExpectStatic, + receiver: string, + deltaBlockNumber = 1 +) { + const currentBlockNumber = await getCurrentBlockNumber(peregrineContext) + + // the inital balance before the incoming switch + const initialBalanceTreasury = await getFreeBalancePeregrineAt( + PeregrineConfig.treasuryAccount, + currentBlockNumber - deltaBlockNumber + ) + const initialBalanceReciver = await getFreeBalancePeregrineAt(receiver, currentBlockNumber - deltaBlockNumber) + + // Current balance + const currentBalanceReciever = await getFreeBalancePeregrine(receiver) + const currentBalanceTreasury = await getFreeBalancePeregrine(PeregrineConfig.treasuryAccount) + + // deltas of the balance between receiver and treasury + const deltaReceivedBalance = currentBalanceReciever - initialBalanceReciver + + // remove staking rewards + const deltaTreasuryBalance = + currentBalanceTreasury - + initialBalanceTreasury - + PeregrineConfig.parachainStakingRewards * BigInt(deltaBlockNumber) + + expect(deltaReceivedBalance + deltaTreasuryBalance).toBe(transferredBalance) +} diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/initiateWithdrawAssetsFromPeregrineAssetHub.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/initiateWithdrawAssetsFromPeregrineAssetHub.test.ts.snap new file mode 100644 index 000000000..5d0e6b211 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/initiateWithdrawAssetsFromPeregrineAssetHub.test.ts.snap @@ -0,0 +1,210 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Initiate withdraw assets Peregrine Account Alice -> AH Account Alice > receiver AssetHub::balances::Burned 1`] = ` +[ + { + "data": { + "amount": 1000000000000, + "who": "13cKp88mpGREFCq8KsJEFjpSBnjFuCNWq6bmD3js7fu4f66e", + }, + "method": "Burned", + "section": "balances", + }, +] +`; + +exports[`Initiate withdraw assets Peregrine Account Alice -> AH Account Alice > receiver AssetHub::balances::Endowed 1`] = ` +[ + { + "data": { + "account": "15jSz35ugoWTc61xHPoxEkHte4o7UanKCk1gx1dizA8yuNs8", + "freeBalance": "(rounded 1000000000000)", + }, + "method": "Endowed", + "section": "balances", + }, +] +`; + +exports[`Initiate withdraw assets Peregrine Account Alice -> AH Account Alice > receiver AssetHub::balances::Minted 1`] = ` +[ + { + "data": { + "amount": "(rounded 1000000000000)", + "who": "15jSz35ugoWTc61xHPoxEkHte4o7UanKCk1gx1dizA8yuNs8", + }, + "method": "Minted", + "section": "balances", + }, +] +`; + +exports[`Initiate withdraw assets Peregrine Account Alice -> AH Account Alice > receiver AssetHub::messageQueue::Processed 1`] = ` +[ + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": "(rounded 2100)", + }, + "success": true, + "weightUsed": { + "proofSize": "(rounded 7200)", + "refTime": "(rounded 320000000)", + }, + }, + "method": "Processed", + "section": "messageQueue", + }, +] +`; + +exports[`Initiate withdraw assets Peregrine Account Alice -> AH Account Alice > sender Peregrine::fungibles::[Burned] 1`] = ` +[ + { + "data": { + "assetId": { + "interior": "Here", + "parents": 1, + }, + "balance": 1000000000000, + "owner": "4seWojfEHrk5YKPahdErazQ3CWEHZYi6NV4gKz5AaejWbRPJ", + }, + "method": "Burned", + "section": "fungibles", + }, +] +`; + +exports[`Initiate withdraw assets Peregrine Account Alice -> AH Account Alice > sender Peregrine::polkadotXcm::[FeesPaid,Attempted,Sent] 1`] = ` +[ + { + "data": { + "outcome": { + "Complete": { + "used": { + "proofSize": 0, + "refTime": 400000000, + }, + }, + }, + }, + "method": "Attempted", + "section": "polkadotXcm", + }, + { + "data": { + "fees": [], + "paying": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + "method": "FeesPaid", + "section": "polkadotXcm", + }, + { + "data": { + "destination": { + "interior": { + "X1": [ + { + "Parachain": 1000, + }, + ], + }, + "parents": 1, + }, + "message": [ + { + "WithdrawAsset": [ + { + "fun": { + "Fungible": 1000000000000, + }, + "id": { + "interior": "Here", + "parents": 1, + }, + }, + ], + }, + "ClearOrigin", + { + "BuyExecution": { + "fees": { + "fun": { + "Fungible": 1000000000000, + }, + "id": { + "interior": "Here", + "parents": 1, + }, + }, + "weightLimit": "Unlimited", + }, + }, + { + "DepositAsset": { + "assets": { + "Wild": { + "AllCounted": 1, + }, + }, + "beneficiary": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + }, + ], + "messageId": "(hash)", + "origin": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + "method": "Sent", + "section": "polkadotXcm", + }, +] +`; + +exports[`Initiate withdraw assets Peregrine Account Alice -> AH Account Alice > sender Peregrine::xcmpQueue::[XcmMessageSent] 1`] = ` +[ + { + "data": { + "messageHash": "(hash)", + }, + "method": "XcmpMessageSent", + "section": "xcmpQueue", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/limitedReserveTransferAssetHubPeregrine.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/limitedReserveTransferAssetHubPeregrine.test.ts.snap new file mode 100644 index 000000000..ca561a26a --- /dev/null +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/limitedReserveTransferAssetHubPeregrine.test.ts.snap @@ -0,0 +1,194 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Limited Reserve V4 Transfers from AssetHub Account Alice -> Peregrine Account Bob > receiver Peregrine::fungibles::[Issued] 1`] = ` +[ + { + "data": { + "amount": "(rounded 1000000000000)", + "assetId": { + "interior": "Here", + "parents": 1, + }, + "owner": "4rsmbFBYpVmWE2LHRsSZKxf22a8cbJLxMZqvVGkGkSDmDBcr", + }, + "method": "Issued", + "section": "fungibles", + }, +] +`; + +exports[`Limited Reserve V4 Transfers from AssetHub Account Alice -> Peregrine Account Bob > receiver Peregrine::messageQueue::[Processed] 1`] = ` +[ + { + "data": { + "id": "(hash)", + "origin": { + "Sibling": 1000, + }, + "success": true, + "weightUsed": { + "proofSize": 0, + "refTime": 1000000000, + }, + }, + "method": "Processed", + "section": "messageQueue", + }, +] +`; + +exports[`Limited Reserve V4 Transfers from AssetHub Account Alice -> Peregrine Account Bob > sender AssetHub::balances::[Withdraw] 1`] = ` +[ + { + "data": { + "amount": "(rounded 24000000)", + "who": "15jSz35ugoWTc61xHPoxEkHte4o7UanKCk1gx1dizA8yuNs8", + }, + "method": "Withdraw", + "section": "balances", + }, +] +`; + +exports[`Limited Reserve V4 Transfers from AssetHub Account Alice -> Peregrine Account Bob > sender AssetHub::polkadotXcm::[FeesPaid,Attempted,Sent] 1`] = ` +[ + { + "data": { + "outcome": { + "Complete": { + "used": { + "proofSize": "(rounded 6200)", + "refTime": "(rounded 290000000)", + }, + }, + }, + }, + "method": "Attempted", + "section": "polkadotXcm", + }, + { + "data": { + "fees": [ + { + "fun": { + "Fungible": "(rounded 310000000)", + }, + "id": { + "interior": "Here", + "parents": 1, + }, + }, + ], + "paying": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": "Polkadot", + }, + }, + ], + }, + "parents": 0, + }, + }, + "method": "FeesPaid", + "section": "polkadotXcm", + }, + { + "data": { + "destination": { + "interior": { + "X1": [ + { + "Parachain": "(rounded 2100)", + }, + ], + }, + "parents": 1, + }, + "message": [ + { + "ReserveAssetDeposited": [ + { + "fun": { + "Fungible": 1000000000000, + }, + "id": { + "interior": "Here", + "parents": 1, + }, + }, + ], + }, + "ClearOrigin", + { + "BuyExecution": { + "fees": { + "fun": { + "Fungible": 1000000000000, + }, + "id": { + "interior": "Here", + "parents": 1, + }, + }, + "weightLimit": "Unlimited", + }, + }, + { + "DepositAsset": { + "assets": { + "Wild": { + "AllCounted": 1, + }, + }, + "beneficiary": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + }, + ], + "messageId": "(hash)", + "origin": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": "Polkadot", + }, + }, + ], + }, + "parents": 0, + }, + }, + "method": "Sent", + "section": "polkadotXcm", + }, +] +`; + +exports[`Limited Reserve V4 Transfers from AssetHub Account Alice -> Peregrine Account Bob > sender AssetHub::xcmpQueue::[XcmMessageSent] 1`] = ` +[ + { + "data": { + "messageHash": "(hash)", + }, + "method": "XcmpMessageSent", + "section": "xcmpQueue", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/limitedReserveTransferRelayPeregrine.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/limitedReserveTransferRelayPeregrine.test.ts.snap new file mode 100644 index 000000000..fbfb8c0f0 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/__snapshots__/limitedReserveTransferRelayPeregrine.test.ts.snap @@ -0,0 +1,146 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Send DOTs from Relay 2 Peregrine > receiver Peregrine::messageQueue::[ProcessingFailed] 1`] = ` +[ + { + "data": { + "error": "Unsupported", + "id": "(hash)", + "origin": "Parent", + }, + "method": "ProcessingFailed", + "section": "messageQueue", + }, +] +`; + +exports[`Send DOTs from Relay 2 Peregrine > sender Rococo::xcmPallet::[XcmMessageSent] 1`] = ` +[ + { + "data": { + "outcome": { + "Complete": { + "used": { + "proofSize": "(rounded 6200)", + "refTime": "(rounded 300000000)", + }, + }, + }, + }, + "method": "Attempted", + "section": "xcmPallet", + }, + { + "data": { + "fees": [ + { + "fun": { + "Fungible": "(rounded 130000000)", + }, + "id": { + "interior": "Here", + "parents": 0, + }, + }, + ], + "paying": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": "Rococo", + }, + }, + ], + }, + "parents": 0, + }, + }, + "method": "FeesPaid", + "section": "xcmPallet", + }, + { + "data": { + "destination": { + "interior": { + "X1": [ + { + "Parachain": "(rounded 2100)", + }, + ], + }, + "parents": 0, + }, + "message": [ + { + "ReserveAssetDeposited": [ + { + "fun": { + "Fungible": 50000000000000, + }, + "id": { + "interior": "Here", + "parents": 1, + }, + }, + ], + }, + "ClearOrigin", + { + "BuyExecution": { + "fees": { + "fun": { + "Fungible": 50000000000000, + }, + "id": { + "interior": "Here", + "parents": 1, + }, + }, + "weightLimit": "Unlimited", + }, + }, + { + "DepositAsset": { + "assets": { + "Wild": { + "AllCounted": 1, + }, + }, + "beneficiary": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": null, + }, + }, + ], + }, + "parents": 0, + }, + }, + }, + ], + "messageId": "(hash)", + "origin": { + "interior": { + "X1": [ + { + "AccountId32": { + "id": "(hash)", + "network": "Rococo", + }, + }, + ], + }, + "parents": 0, + }, + }, + "method": "Sent", + "section": "xcmPallet", + }, +] +`; diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/initiateWithdrawAssetsFromPeregrineAssetHub.test.ts b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/initiateWithdrawAssetsFromPeregrineAssetHub.test.ts new file mode 100644 index 000000000..67551b688 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/initiateWithdrawAssetsFromPeregrineAssetHub.test.ts @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { test } from 'vitest' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +import * as PeregrineConfig from '../../../../network/peregrine.js' +import * as AssetHubConfig from '../../../../network/assetHub.js' +import { + ROC, + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysCharlie, +} from '../../../../utils.js' +import { peregrineContext, assethubContext, getFreeRocPeregrine, getFreeRocAssetHub } from '../../../index.js' +import { + getAccountLocationV4, + getRelayNativeAssetIdLocationV4, + getSiblingLocationV4, +} from '../../../../network/utils.js' +import { checkBalanceInRange, createBlock, hexAddress, setStorage } from '../../../utils.js' + +test('Initiate withdraw assets Peregrine Account Alice -> AH Account Alice', async ({ expect }) => { + const { checkEvents, checkSystemEvents } = withExpect(expect) + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, [keysAlice.address]), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + const switchParameters = getAssetSwitchParameters() + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(switchParameters, remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage( + [PeregrineConfig.sovereignAccountAsSibling], + initialBalanceROC + ), + }) + + // check initial state + const balanceAliceRocPeregrineBeforeTx = await getFreeRocPeregrine(keysAlice.address) + const balanceAliceRocAssetHubBeforeTx = await getFreeRocAssetHub(keysAlice.address) + const balanceSovereignAccountAssetHubBeforeTx = await getFreeRocAssetHub(PeregrineConfig.sovereignAccountAsSibling) + + expect(balanceAliceRocPeregrineBeforeTx).toBe(initialBalanceROC) + expect(balanceAliceRocAssetHubBeforeTx).toBe(BigInt(0)) + expect(balanceSovereignAccountAssetHubBeforeTx).toBe(initialBalanceROC) + + const assetHubDestination = getSiblingLocationV4(AssetHubConfig.paraId) + // We send 1 ROC + const assets = { V4: [getRelayNativeAssetIdLocationV4(ROC.toString())] } + const beneficiary = getAccountLocationV4(hexAddress(keysAlice.address)) + + const signedTx4 = peregrineContext.api.tx.polkadotXcm + .transferAssets(assetHubDestination, beneficiary, assets, 0, 'Unlimited') + .signAsync(keysAlice) + + const events4 = await sendTransaction(signedTx4) + await createBlock(peregrineContext) + + // The xcm message should be send to AH and the funds should be burned from user. + await checkEvents(events4, 'fungibles').toMatchSnapshot('sender Peregrine::fungibles::[Burned]') + await checkEvents(events4, 'xcmpQueue').toMatchSnapshot('sender Peregrine::xcmpQueue::[XcmMessageSent]') + await checkEvents(events4, 'polkadotXcm').toMatchSnapshot( + 'sender Peregrine::polkadotXcm::[FeesPaid,Attempted,Sent]' + ) + + // Alice funds after the transaction + const balanceAliceRocPeregrineAfterTx = await getFreeRocPeregrine(keysAlice.address) + expect(balanceAliceRocPeregrineAfterTx).toBe(initialBalanceROC - ROC) + + // The funds should be burned from Sovereign account and minted to user. + await createBlock(assethubContext) + await checkSystemEvents(assethubContext, { section: 'balances', method: 'Burned' }).toMatchSnapshot( + 'receiver AssetHub::balances::Burned' + ) + await checkSystemEvents(assethubContext, { section: 'balances', method: 'Minted' }).toMatchSnapshot( + 'receiver AssetHub::balances::Minted' + ) + await checkSystemEvents(assethubContext, { section: 'balances', method: 'Endowed' }).toMatchSnapshot( + 'receiver AssetHub::balances::Endowed' + ) + await checkSystemEvents(assethubContext, { section: 'messageQueue', method: 'Processed' }).toMatchSnapshot( + 'receiver AssetHub::messageQueue::Processed' + ) + + // state sovereign account + const balanceSovereignAccountAssetHubAfterTx = await getFreeRocAssetHub(PeregrineConfig.sovereignAccountAsSibling) + expect(balanceSovereignAccountAssetHubAfterTx).toBe(initialBalanceROC - ROC) + + // state alice on asset hub + await checkBalanceInRange(getFreeRocPeregrine, keysAlice.address, expect, [BigInt(999999964195), ROC]) +}, 20_000) diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/limitedReserveTransferAssetHubPeregrine.test.ts b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/limitedReserveTransferAssetHubPeregrine.test.ts new file mode 100644 index 000000000..f19db9037 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/limitedReserveTransferAssetHubPeregrine.test.ts @@ -0,0 +1,104 @@ +import { test } from 'vitest' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +import * as PeregrineConfig from '../../../../network/peregrine.js' +import * as AssetHubConfig from '../../../../network/assetHub.js' +import { + ROC, + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysBob, + keysCharlie, +} from '../../../../utils.js' +import { peregrineContext, assethubContext, getFreeRocPeregrine, getFreeRocAssetHub } from '../../../index.js' +import { + getAccountLocationV4, + getRelayNativeAssetIdLocationV4, + getSiblingLocationV4, +} from '../../../../network/utils.js' +import { checkBalance, checkBalanceInRange, createBlock, hexAddress, setStorage } from '../../../utils.js' + +const ROC_ASSET_V4 = { V4: [getRelayNativeAssetIdLocationV4(ROC)] } + +test('Limited Reserve V4 Transfers from AssetHub Account Alice -> Peregrine Account Bob', async ({ expect }) => { + const { checkEvents, checkSystemEvents } = withExpect(expect) + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + // Assign alice some KILT tokens to create the account + await setStorage(peregrineContext, { + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, []), + ...PeregrineConfig.assignNativeTokensToAccounts([keysBob.address], initialBalanceKILT), + }) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(getAssetSwitchParameters(), remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + // Give Alice some Rocs + await setStorage( + assethubContext, + AssetHubConfig.assignDotTokensToAccountsAsStorage([keysAlice.address], initialBalanceROC) + ) + + const peregrineSovereignAccountBalanceBeforeTx = await getFreeRocAssetHub(PeregrineConfig.sovereignAccountAsSibling) + + // Bob should have no ROCs on Peregrine + await checkBalance(getFreeRocPeregrine, keysBob.address, expect, BigInt(0)) + + // Alice should some ROCs on AH + await checkBalance(getFreeRocAssetHub, keysAlice.address, expect, initialBalanceROC) + + const bobAddress = hexAddress(keysBob.address) + const peregrineDestination = getSiblingLocationV4(PeregrineConfig.paraId) + const beneficiary = getAccountLocationV4(bobAddress) + + const signedTx = assethubContext.api.tx.polkadotXcm + .limitedReserveTransferAssets(peregrineDestination, beneficiary, ROC_ASSET_V4, 0, 'Unlimited') + .signAsync(keysAlice) + + const events = await sendTransaction(signedTx) + + // Check sender state + await createBlock(assethubContext) + + // Check events sender + checkEvents(events, 'xcmpQueue').toMatchSnapshot('sender AssetHub::xcmpQueue::[XcmMessageSent]') + checkEvents(events, 'polkadotXcm').toMatchSnapshot('sender AssetHub::polkadotXcm::[FeesPaid,Attempted,Sent]') + checkEvents(events, { section: 'balances', method: 'Withdraw' }).toMatchSnapshot( + 'sender AssetHub::balances::[Withdraw]' + ) + + // check balance. The sovereign account should hold one additional ROC. + await checkBalance( + getFreeRocAssetHub, + PeregrineConfig.sovereignAccountAsSibling, + expect, + peregrineSovereignAccountBalanceBeforeTx + ROC + ) + + // check balance sender + // Equal to `initialBalanceKILT - KILT` - tx fees + await checkBalanceInRange(getFreeRocAssetHub, keysAlice.address, expect, [ + BigInt('98999830999996'), + BigInt('98999830999996'), + ]) + + // Check receiver state + await createBlock(peregrineContext) + + // Check events receiver + checkSystemEvents(peregrineContext, { section: 'fungibles', method: 'Issued' }).toMatchSnapshot( + 'receiver Peregrine::fungibles::[Issued]' + ) + checkSystemEvents(peregrineContext, 'messageQueue').toMatchSnapshot('receiver Peregrine::messageQueue::[Processed]') + + // check balance receiver + // check balance. Equal to `KILT` - tx fees + await checkBalanceInRange(getFreeRocPeregrine, bobAddress, expect, [BigInt(999999964195), BigInt(999999964296)]) +}, 20_000) diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/limitedReserveTransferRelayPeregrine.test.ts b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/limitedReserveTransferRelayPeregrine.test.ts new file mode 100644 index 000000000..ca3c7a560 --- /dev/null +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/peregrine/limitedReserveTransferRelayPeregrine.test.ts @@ -0,0 +1,75 @@ +import { test } from 'vitest' +import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' + +import * as PeregrineConfig from '../../../../network/peregrine.js' +import * as RococoConfig from '../../../../network/rococo.js' +import * as AssetHubConfig from '../../../../network/assetHub.js' +import { + getAssetSwitchParameters, + initialBalanceKILT, + initialBalanceROC, + keysAlice, + keysCharlie, + ROC, +} from '../../../../utils.js' +import { peregrineContext, getFreeBalancePeregrine, getFreeRocPeregrine, rococoContext } from '../../../index.js' +import { checkBalance, createBlock, setStorage, hexAddress } from '../../../utils.js' +import { + getAccountLocationV3, + getChildLocation, + getNativeAssetIdLocationV3, + getSiblingLocationV4, +} from '../../../../network/utils.js' + +test('Send DOTs from Relay 2 Peregrine', async ({ expect }) => { + const { checkEvents, checkSystemEvents } = withExpect(expect) + + const feeAmount = (ROC * BigInt(10)) / BigInt(100) + const remoteAssetId = { V4: AssetHubConfig.eKiltLocation } + const remoteXcmFeeId = { V4: { id: AssetHubConfig.nativeTokenLocation, fun: { Fungible: feeAmount } } } + const remoteReserveLocation = getSiblingLocationV4(AssetHubConfig.paraId) + + await setStorage(peregrineContext, { + ...PeregrineConfig.assignNativeTokensToAccounts([keysAlice.address], initialBalanceKILT), + ...PeregrineConfig.createAndAssignRocs(keysCharlie.address, []), + ...PeregrineConfig.setSafeXcmVersion4(), + }) + + await setStorage( + peregrineContext, + PeregrineConfig.setSwitchPair(getAssetSwitchParameters(), remoteAssetId, remoteXcmFeeId, remoteReserveLocation) + ) + + await setStorage(rococoContext, RococoConfig.assignNativeTokensToAccounts([keysAlice.address])) + + await checkBalance(getFreeBalancePeregrine, keysAlice.address, expect, initialBalanceKILT) + await checkBalance(getFreeRocPeregrine, keysAlice.address, expect, BigInt(0)) + + const balanceToTransfer = initialBalanceROC / BigInt(2) + + const aliceAddress = hexAddress(keysAlice.address) + const hydraDxDestination = { V3: getChildLocation(PeregrineConfig.paraId) } + const beneficiary = getAccountLocationV3(aliceAddress) + const assetToTransfer = { V3: [getNativeAssetIdLocationV3(balanceToTransfer)] } + + const signedTx = rococoContext.api.tx.xcmPallet + .limitedReserveTransferAssets(hydraDxDestination, beneficiary, assetToTransfer, 0, 'Unlimited') + .signAsync(keysAlice) + + const events = await sendTransaction(signedTx) + + await createBlock(rococoContext) + + checkEvents(events, 'xcmPallet').toMatchSnapshot('sender Rococo::xcmPallet::[XcmMessageSent]') + + await createBlock(peregrineContext) + + // Barrier will block execution. No event will be emitted. + await checkSystemEvents(peregrineContext, { + section: 'messageQueue', + method: 'ProcessingFailed', + }).toMatchSnapshot('receiver Peregrine::messageQueue::[ProcessingFailed]') + + // Alice should still have no balance + await checkBalance(getFreeRocPeregrine, keysAlice.address, expect, BigInt(0)) +}, 20_00000) diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/__snapshots__/limitedReserveTransferAssetHubSpiritnet.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/__snapshots__/limitedReserveTransferAssetHubSpiritnet.test.ts.snap similarity index 100% rename from integration-tests/chopsticks/src/tests/xcm/assetHub/__snapshots__/limitedReserveTransferAssetHubSpiritnet.test.ts.snap rename to integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/__snapshots__/limitedReserveTransferAssetHubSpiritnet.test.ts.snap diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/__snapshots__/limitedReserveTransferSpiritnetAssetHub.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/__snapshots__/limitedReserveTransferSpiritnetAssetHub.test.ts.snap similarity index 100% rename from integration-tests/chopsticks/src/tests/xcm/assetHub/__snapshots__/limitedReserveTransferSpiritnetAssetHub.test.ts.snap rename to integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/__snapshots__/limitedReserveTransferSpiritnetAssetHub.test.ts.snap diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/__snapshots__/teleportTransferAssetHubSpiritnet.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/__snapshots__/teleportTransferAssetHubSpiritnet.test.ts.snap similarity index 100% rename from integration-tests/chopsticks/src/tests/xcm/assetHub/__snapshots__/teleportTransferAssetHubSpiritnet.test.ts.snap rename to integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/__snapshots__/teleportTransferAssetHubSpiritnet.test.ts.snap diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/__snapshots__/teleportTransferSpiritnetAssetHub.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/__snapshots__/teleportTransferSpiritnetAssetHub.test.ts.snap similarity index 100% rename from integration-tests/chopsticks/src/tests/xcm/assetHub/__snapshots__/teleportTransferSpiritnetAssetHub.test.ts.snap rename to integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/__snapshots__/teleportTransferSpiritnetAssetHub.test.ts.snap diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/limitedReserveTransferAssetHubSpiritnet.test.ts b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/limitedReserveTransferAssetHubSpiritnet.test.ts similarity index 73% rename from integration-tests/chopsticks/src/tests/xcm/assetHub/limitedReserveTransferAssetHubSpiritnet.test.ts rename to integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/limitedReserveTransferAssetHubSpiritnet.test.ts index cd894936a..d2bfda700 100644 --- a/integration-tests/chopsticks/src/tests/xcm/assetHub/limitedReserveTransferAssetHubSpiritnet.test.ts +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/limitedReserveTransferAssetHubSpiritnet.test.ts @@ -1,23 +1,23 @@ import { test } from 'vitest' import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' -import * as SpiritnetConfig from '../../../network/spiritnet.js' -import * as AssetHubConfig from '../../../network/assetHub.js' -import { DOT, keysAlice } from '../../../utils.js' -import { spiritnetContext, assetHubContext } from '../../index.js' -import { getSiblingLocation } from '../../../network/utils.js' -import { createBlock, hexAddress, setStorage } from '../../utils.js' +import * as SpiritnetConfig from '../../../../network/spiritnet.js' +import * as AssetHubConfig from '../../../../network/assetHub.js' +import { DOT, keysAlice } from '../../../../utils.js' +import { spiritnetContext, assethubContext } from '../../../index.js' +import { getSiblingLocationV3 } from '../../../../network/utils.js' +import { createBlock, hexAddress, setStorage } from '../../../utils.js' test('Limited Reserve Transfers from AH Account Alice -> Spiritnet Account Alice', async ({ expect }) => { const { checkEvents, checkSystemEvents } = withExpect(expect) // Assign alice some KSM - await setStorage(assetHubContext, { - ...AssetHubConfig.assignDotTokensToAccounts([keysAlice.address]), + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage([keysAlice.address]), ...AssetHubConfig.assignKSMtoAccounts([keysAlice.address]), }) - const spiritnetDestination = { V3: getSiblingLocation(SpiritnetConfig.paraId) } + const spiritnetDestination = { V3: getSiblingLocationV3(SpiritnetConfig.paraId) } const KSMAsset = { V3: [{ id: { Concrete: AssetHubConfig.KSMAssetLocation }, fun: { Fungible: DOT.toString() } }] } const remoteFeeId = { V3: { Concrete: AssetHubConfig.KSMAssetLocation } } const xcmMessage = { @@ -41,7 +41,7 @@ test('Limited Reserve Transfers from AH Account Alice -> Spiritnet Account Alice } // Otherwise the it is tried to route the msg over KSM. - const signedTx = assetHubContext.api.tx.polkadotXcm + const signedTx = assethubContext.api.tx.polkadotXcm .transferAssetsUsingTypeAndThen( spiritnetDestination, KSMAsset, @@ -54,7 +54,7 @@ test('Limited Reserve Transfers from AH Account Alice -> Spiritnet Account Alice .signAsync(keysAlice) const events = await sendTransaction(signedTx) - await createBlock(assetHubContext) + await createBlock(assethubContext) // MSG should still be send. await checkEvents(events, 'xcmpQueue').toMatchSnapshot( diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/limitedReserveTransferSpiritnetAssetHub.test.ts b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/limitedReserveTransferSpiritnetAssetHub.test.ts similarity index 75% rename from integration-tests/chopsticks/src/tests/xcm/assetHub/limitedReserveTransferSpiritnetAssetHub.test.ts rename to integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/limitedReserveTransferSpiritnetAssetHub.test.ts index 7d1123309..fc8a6e3d1 100644 --- a/integration-tests/chopsticks/src/tests/xcm/assetHub/limitedReserveTransferSpiritnetAssetHub.test.ts +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/limitedReserveTransferSpiritnetAssetHub.test.ts @@ -1,12 +1,12 @@ import { test } from 'vitest' import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' -import * as SpiritnetConfig from '../../../network/spiritnet.js' -import * as AssetHubConfig from '../../../network/assetHub.js' -import { KILT, initialBalanceKILT, keysAlice } from '../../../utils.js' -import { spiritnetContext, assetHubContext, getFreeBalanceSpiritnet } from '../../index.js' -import { getAccountLocationV3, getNativeAssetIdLocation, getSiblingLocation } from '../../../network/utils.js' -import { checkBalance, createBlock, hexAddress, setStorage } from '../../utils.js' +import * as SpiritnetConfig from '../../../../network/spiritnet.js' +import * as AssetHubConfig from '../../../../network/assetHub.js' +import { KILT, initialBalanceKILT, keysAlice } from '../../../../utils.js' +import { spiritnetContext, assethubContext, getFreeBalanceSpiritnet } from '../../../index.js' +import { getAccountLocationV3, getNativeAssetIdLocationV3, getSiblingLocationV3 } from '../../../../network/utils.js' +import { checkBalance, createBlock, hexAddress, setStorage } from '../../../utils.js' test('Limited Reserve Transfers from Spiritnet Account Alice -> AH Account Alice', async ({ expect }) => { const { checkEvents, checkSystemEvents } = withExpect(expect) @@ -26,9 +26,9 @@ test('Limited Reserve Transfers from Spiritnet Account Alice -> AH Account Alice await checkBalance(getFreeBalanceSpiritnet, keysAlice.address, expect, initialBalanceKILT) const aliceAddress = hexAddress(keysAlice.address) - const assetHubDestination = { V3: getSiblingLocation(AssetHubConfig.paraId) } + const assetHubDestination = { V3: getSiblingLocationV3(AssetHubConfig.paraId) } const beneficiary = getAccountLocationV3(aliceAddress) - const kiltAsset = { V3: [getNativeAssetIdLocation(KILT.toString())] } + const kiltAsset = { V3: [getNativeAssetIdLocationV3(KILT.toString())] } const signedTx = spiritnetContext.api.tx.polkadotXcm .limitedReserveTransferAssets(assetHubDestination, beneficiary, kiltAsset, 0, 'Unlimited') @@ -58,10 +58,10 @@ test('Limited Reserve Transfers from Spiritnet Account Alice -> AH Account Alice assetHubSovereignAccountBalance + KILT ) - await createBlock(assetHubContext) + await createBlock(assethubContext) // MSG processing will fail on AH. - await checkSystemEvents(assetHubContext, 'messageQueue').toMatchSnapshot( + await checkSystemEvents(assethubContext, 'messageQueue').toMatchSnapshot( `receiver assetHub::messageQueue::[Processed] asset ${JSON.stringify(kiltAsset)}` ) }, 20_000) diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/teleportTransferAssetHubSpiritnet.test.ts b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/teleportTransferAssetHubSpiritnet.test.ts similarity index 73% rename from integration-tests/chopsticks/src/tests/xcm/assetHub/teleportTransferAssetHubSpiritnet.test.ts rename to integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/teleportTransferAssetHubSpiritnet.test.ts index ea5cc57fd..7c9e2a391 100644 --- a/integration-tests/chopsticks/src/tests/xcm/assetHub/teleportTransferAssetHubSpiritnet.test.ts +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/teleportTransferAssetHubSpiritnet.test.ts @@ -1,23 +1,23 @@ import { test } from 'vitest' import { sendTransaction, withExpect } from '@acala-network/chopsticks-testing' -import * as SpiritnetConfig from '../../../network/spiritnet.js' -import * as AssetHubConfig from '../../../network/assetHub.js' -import { DOT, keysAlice } from '../../../utils.js' -import { spiritnetContext, assetHubContext } from '../../index.js' -import { getSiblingLocation } from '../../../network/utils.js' -import { createBlock, hexAddress, setStorage } from '../../utils.js' +import * as SpiritnetConfig from '../../../../network/spiritnet.js' +import * as AssetHubConfig from '../../../../network/assetHub.js' +import { DOT, keysAlice } from '../../../../utils.js' +import { spiritnetContext, assethubContext } from '../../../index.js' +import { getSiblingLocationV3 } from '../../../../network/utils.js' +import { createBlock, hexAddress, setStorage } from '../../../utils.js' test('Teleport Transfers from AH Account Alice -> Spiritnet Account Alice', async ({ expect }) => { const { checkEvents, checkSystemEvents } = withExpect(expect) // Assign alice some KSM - await setStorage(assetHubContext, { - ...AssetHubConfig.assignDotTokensToAccounts([keysAlice.address]), + await setStorage(assethubContext, { + ...AssetHubConfig.assignDotTokensToAccountsAsStorage([keysAlice.address]), ...AssetHubConfig.assignKSMtoAccounts([keysAlice.address]), }) - const spiritnetDestination = { V3: getSiblingLocation(SpiritnetConfig.paraId) } + const spiritnetDestination = { V3: getSiblingLocationV3(SpiritnetConfig.paraId) } const KSMAsset = { V3: [{ id: { Concrete: AssetHubConfig.KSMAssetLocation }, fun: { Fungible: DOT.toString() } }] } const remoteFeeId = { V3: { Concrete: AssetHubConfig.KSMAssetLocation } } const xcmMessage = { @@ -41,7 +41,7 @@ test('Teleport Transfers from AH Account Alice -> Spiritnet Account Alice', asyn } // Otherwise the it is tried to route the msg over KSM. - const signedTx = assetHubContext.api.tx.polkadotXcm + const signedTx = assethubContext.api.tx.polkadotXcm .transferAssetsUsingTypeAndThen( spiritnetDestination, KSMAsset, @@ -54,7 +54,7 @@ test('Teleport Transfers from AH Account Alice -> Spiritnet Account Alice', asyn .signAsync(keysAlice) const events = await sendTransaction(signedTx) - await createBlock(assetHubContext) + await createBlock(assethubContext) // MSG should still be send. await checkEvents(events, 'xcmpQueue').toMatchSnapshot( diff --git a/integration-tests/chopsticks/src/tests/xcm/assetHub/teleportTransferSpiritnetAssetHub.test.ts b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/teleportTransferSpiritnetAssetHub.test.ts similarity index 67% rename from integration-tests/chopsticks/src/tests/xcm/assetHub/teleportTransferSpiritnetAssetHub.test.ts rename to integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/teleportTransferSpiritnetAssetHub.test.ts index 93789b147..2f3c5980e 100644 --- a/integration-tests/chopsticks/src/tests/xcm/assetHub/teleportTransferSpiritnetAssetHub.test.ts +++ b/integration-tests/chopsticks/src/tests/xcm/assetHub/spiritnet/teleportTransferSpiritnetAssetHub.test.ts @@ -1,11 +1,11 @@ import { test } from 'vitest' -import * as SpiritnetConfig from '../../../network/spiritnet.js' -import * as AssetHubConfig from '../../../network/assetHub.js' -import { KILT, initialBalanceKILT, keysAlice } from '../../../utils.js' -import { spiritnetContext, getFreeBalanceSpiritnet } from '../../index.js' -import { getAccountLocationV3, getNativeAssetIdLocation, getSiblingLocation } from '../../../network/utils.js' -import { checkBalance, createBlock, hexAddress, setStorage } from '../../utils.js' +import * as SpiritnetConfig from '../../../../network/spiritnet.js' +import * as AssetHubConfig from '../../../../network/assetHub.js' +import { KILT, initialBalanceKILT, keysAlice } from '../../../../utils.js' +import { spiritnetContext, getFreeBalanceSpiritnet } from '../../../index.js' +import { getAccountLocationV3, getNativeAssetIdLocationV3, getSiblingLocationV3 } from '../../../../network/utils.js' +import { checkBalance, createBlock, hexAddress, setStorage } from '../../../utils.js' test('Teleport assets from Spiritnet Account Alice -> AH Account Alice', async ({ expect }) => { // Assign alice some KILT tokens @@ -18,12 +18,12 @@ test('Teleport assets from Spiritnet Account Alice -> AH Account Alice', async ( await checkBalance(getFreeBalanceSpiritnet, keysAlice.address, expect, initialBalanceKILT) const aliceAddress = hexAddress(keysAlice.address) - const assetHubDestination = { V3: getSiblingLocation(AssetHubConfig.paraId) } + const assetHubDestination = { V3: getSiblingLocationV3(AssetHubConfig.paraId) } const beneficiary = getAccountLocationV3(aliceAddress) - const kiltAsset = { V3: [getNativeAssetIdLocation(KILT)] } + const kiltAsset = { V3: [getNativeAssetIdLocationV3(KILT)] } // Teleportation should exhaust resources. This is intended until isTeleport is enabled in the XCM config. - expect(async () => { + await expect(async () => { await spiritnetContext.api.tx.polkadotXcm .teleportAssets(assetHubDestination, beneficiary, kiltAsset, 0) .signAndSend(keysAlice) diff --git a/integration-tests/chopsticks/src/tests/xcm/hydration/__snapshots__/limitedReserveTransferSpiritnetHydraDxV2.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/hydration/__snapshots__/limitedReserveTransferSpiritnetHydraDxV2.test.ts.snap index cb5b551fc..4aae740e4 100644 --- a/integration-tests/chopsticks/src/tests/xcm/hydration/__snapshots__/limitedReserveTransferSpiritnetHydraDxV2.test.ts.snap +++ b/integration-tests/chopsticks/src/tests/xcm/hydration/__snapshots__/limitedReserveTransferSpiritnetHydraDxV2.test.ts.snap @@ -13,7 +13,7 @@ exports[`Limited Reserve V2 Transfers from Spiritnet Account Alice -> HydraDx Ac }, { "data": { - "amount": "(rounded 3500000000000)", + "amount": "(rounded 3200000000000)", "currencyId": 28, "who": "7L53bUTBopuwFt3mKUfmkzgGLayYa1Yvn1hAg9v5UMrQzTfh", }, diff --git a/integration-tests/chopsticks/src/tests/xcm/hydration/__snapshots__/limitedReserveTransferSpiritnetHydraDxV3.test.ts.snap b/integration-tests/chopsticks/src/tests/xcm/hydration/__snapshots__/limitedReserveTransferSpiritnetHydraDxV3.test.ts.snap index f1d3f0e48..cd07dfd9b 100644 --- a/integration-tests/chopsticks/src/tests/xcm/hydration/__snapshots__/limitedReserveTransferSpiritnetHydraDxV3.test.ts.snap +++ b/integration-tests/chopsticks/src/tests/xcm/hydration/__snapshots__/limitedReserveTransferSpiritnetHydraDxV3.test.ts.snap @@ -13,7 +13,7 @@ exports[`Limited Reserve V3 Transfers from Spiritnet Account Alice -> HydraDx Ac }, { "data": { - "amount": "(rounded 3500000000000)", + "amount": "(rounded 3200000000000)", "currencyId": 28, "who": "7L53bUTBopuwFt3mKUfmkzgGLayYa1Yvn1hAg9v5UMrQzTfh", }, diff --git a/integration-tests/chopsticks/src/tests/xcm/hydration/limitedReserveTransferSpiritnetHydraDxV2.test.ts b/integration-tests/chopsticks/src/tests/xcm/hydration/limitedReserveTransferSpiritnetHydraDxV2.test.ts index f8a9674e2..9de60e4f5 100644 --- a/integration-tests/chopsticks/src/tests/xcm/hydration/limitedReserveTransferSpiritnetHydraDxV2.test.ts +++ b/integration-tests/chopsticks/src/tests/xcm/hydration/limitedReserveTransferSpiritnetHydraDxV2.test.ts @@ -5,10 +5,11 @@ import * as SpiritnetConfig from '../../../network/spiritnet.js' import * as HydraDxConfig from '../../../network/hydraDx.js' import { KILT, initialBalanceKILT, keysAlice } from '../../../utils.js' import { spiritnetContext, hydradxContext, getFreeBalanceSpiritnet, getFreeBalanceHydraDxKilt } from '../../index.js' -import { getAccountLocationV2, getNativeAssetIdLocation, getSiblingLocation } from '../../../network/utils.js' +import { getAccountLocationV2, getNativeAssetIdLocationV3, getSiblingLocationV3 } from '../../../network/utils.js' import { checkBalance, checkBalanceInRange, createBlock, hexAddress, setStorage } from '../../utils.js' -const KILT_ASSET_V2 = { V2: [getNativeAssetIdLocation(KILT)] } +// native asset location is the same for V2 and V3 +const KILT_ASSET_V2 = { V2: [getNativeAssetIdLocationV3(KILT)] } test('Limited Reserve V2 Transfers from Spiritnet Account Alice -> HydraDx Account Alice', async ({ expect }) => { const { checkEvents, checkSystemEvents } = withExpect(expect) @@ -31,7 +32,7 @@ test('Limited Reserve V2 Transfers from Spiritnet Account Alice -> HydraDx Accou await checkBalance(getFreeBalanceHydraDxKilt, keysAlice.address, expect, BigInt(0)) const aliceAddress = hexAddress(keysAlice.address) - const hydraDxDestination = { V2: getSiblingLocation(HydraDxConfig.paraId) } + const hydraDxDestination = { V2: getSiblingLocationV3(HydraDxConfig.paraId) } const beneficiary = getAccountLocationV2(aliceAddress) const signedTx = spiritnetContext.api.tx.polkadotXcm diff --git a/integration-tests/chopsticks/src/tests/xcm/hydration/limitedReserveTransferSpiritnetHydraDxV3.test.ts b/integration-tests/chopsticks/src/tests/xcm/hydration/limitedReserveTransferSpiritnetHydraDxV3.test.ts index f2c2bdc02..a82c3908f 100644 --- a/integration-tests/chopsticks/src/tests/xcm/hydration/limitedReserveTransferSpiritnetHydraDxV3.test.ts +++ b/integration-tests/chopsticks/src/tests/xcm/hydration/limitedReserveTransferSpiritnetHydraDxV3.test.ts @@ -5,10 +5,10 @@ import * as SpiritnetConfig from '../../../network/spiritnet.js' import * as HydraDxConfig from '../../../network/hydraDx.js' import { KILT, initialBalanceKILT, keysAlice } from '../../../utils.js' import { spiritnetContext, hydradxContext, getFreeBalanceSpiritnet, getFreeBalanceHydraDxKilt } from '../../index.js' -import { getAccountLocationV3, getNativeAssetIdLocation, getSiblingLocation } from '../../../network/utils.js' +import { getAccountLocationV3, getNativeAssetIdLocationV3, getSiblingLocationV3 } from '../../../network/utils.js' import { checkBalance, checkBalanceInRange, createBlock, hexAddress, setStorage } from '../../utils.js' -const KILT_ASSET_V3 = { V3: [getNativeAssetIdLocation(KILT)] } +const KILT_ASSET_V3 = { V3: [getNativeAssetIdLocationV3(KILT)] } test('Limited Reserve V3 Transfers from Spiritnet Account Alice -> HydraDx Account Alice', async ({ expect }) => { const { checkEvents, checkSystemEvents } = withExpect(expect) @@ -30,7 +30,7 @@ test('Limited Reserve V3 Transfers from Spiritnet Account Alice -> HydraDx Accou await checkBalance(getFreeBalanceHydraDxKilt, keysAlice.address, expect, BigInt(0)) const aliceAddress = hexAddress(keysAlice.address) - const hydraDxDestination = { V3: getSiblingLocation(HydraDxConfig.paraId) } + const hydraDxDestination = { V3: getSiblingLocationV3(HydraDxConfig.paraId) } const beneficiary = getAccountLocationV3(aliceAddress) const signedTx = spiritnetContext.api.tx.polkadotXcm diff --git a/integration-tests/chopsticks/src/types.ts b/integration-tests/chopsticks/src/types.ts new file mode 100644 index 000000000..99274a644 --- /dev/null +++ b/integration-tests/chopsticks/src/types.ts @@ -0,0 +1,5 @@ +export interface AssetSwitchSupplyParameters { + circulatingSupply: bigint + sovereignSupply: bigint + totalSupply: bigint +} diff --git a/integration-tests/chopsticks/src/utils.ts b/integration-tests/chopsticks/src/utils.ts index 272d444f3..deb7ba87e 100644 --- a/integration-tests/chopsticks/src/utils.ts +++ b/integration-tests/chopsticks/src/utils.ts @@ -1,5 +1,7 @@ import { Keyring } from '@polkadot/keyring' +import { AssetSwitchSupplyParameters } from './types.js' + const keyring = new Keyring({ type: 'ed25519', ss58Format: 38 }) export const keysAlice = keyring.addFromUri('//alice', undefined, 'ed25519') @@ -14,10 +16,21 @@ export function toNumber(value: string | undefined): number | undefined { return Number(value) } +export function getAssetSwitchParameters( + totalSupply: bigint = BigInt('100000000000000000000'), + ratioCirculating: bigint = BigInt(10) +): AssetSwitchSupplyParameters { + const circulatingSupply = (totalSupply * ratioCirculating) / BigInt(100) + const sovereignSupply = totalSupply - circulatingSupply + return { circulatingSupply, sovereignSupply, totalSupply } +} + export const KILT = BigInt(1e15) export const DOT = BigInt(1e10) export const HDX = BigInt(1e12) +export const ROC = BigInt(1e12) export const initialBalanceKILT = BigInt(100) * KILT export const initialBalanceDOT = BigInt(100) * DOT export const initialBalanceHDX = BigInt(100) * HDX +export const initialBalanceROC = BigInt(100) * ROC diff --git a/integration-tests/chopsticks/yarn.lock b/integration-tests/chopsticks/yarn.lock index d835f62c5..e6eb16805 100644 --- a/integration-tests/chopsticks/yarn.lock +++ b/integration-tests/chopsticks/yarn.lock @@ -7,16 +7,16 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@acala-network/chopsticks-core@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-core/-/chopsticks-core-0.10.0.tgz#0191a4ca9739b84f07e4267cb5e7d0ca7eccb7b9" - integrity sha512-JoxlHLLpKgs/STdG4xh8KkMIWM0Ssjo1aeMEoW8G0zgKcgGqUdrsMGtJoHsxxTFOYvsZCavV37ukGLZFmq1zMg== - dependencies: - "@acala-network/chopsticks-executor" "0.10.0" - "@polkadot/rpc-provider" "^10.11.2" - "@polkadot/types" "^10.11.2" - "@polkadot/types-codec" "^10.11.2" - "@polkadot/types-known" "^10.11.2" +"@acala-network/chopsticks-core@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-core/-/chopsticks-core-0.12.2.tgz#31f163eb328b0600eddc0925c83684c4262ddd74" + integrity sha512-OwiBPtCcwrDrvINX8YJPQ2LOMUKwdJeLq0dBxeXabgaDeQWZHfgTPlI6U0/VEbp+UnRIkxr+WESHJYI69wseDQ== + dependencies: + "@acala-network/chopsticks-executor" "0.12.2" + "@polkadot/rpc-provider" "^11.2.1" + "@polkadot/types" "^11.2.1" + "@polkadot/types-codec" "^11.2.1" + "@polkadot/types-known" "^11.2.1" "@polkadot/util" "^12.6.2" "@polkadot/util-crypto" "^12.6.2" comlink "^4.4.1" @@ -28,118 +28,62 @@ rxjs "^7.8.1" zod "^3.22.4" -"@acala-network/chopsticks-core@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-core/-/chopsticks-core-0.10.1.tgz#11920c95db7f609d26b93d57ea1d30732b8467f0" - integrity sha512-AmGRoUzbmVHy8rZL6nA+CtiP4GZw/4DGTN+srqPcaWPq/hGKYKjhfw6yOUS/Ln7wukbTGfoy0pP9FUNwFAOt4g== +"@acala-network/chopsticks-db@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-db/-/chopsticks-db-0.12.2.tgz#b34217074a41267e199984ef27a9bbad256972e2" + integrity sha512-ViyFr9GcQpKqf0leJ/T4hJcatgjLbQOktytgBeIOu27TcTjhZjKPBBl+2cyREk6H9AQlO5fZjCQzwkqoNP1zyg== dependencies: - "@acala-network/chopsticks-executor" "0.10.1" - "@polkadot/rpc-provider" "^10.11.2" - "@polkadot/types" "^10.11.2" - "@polkadot/types-codec" "^10.11.2" - "@polkadot/types-known" "^10.11.2" - "@polkadot/util" "^12.6.2" - "@polkadot/util-crypto" "^12.6.2" - comlink "^4.4.1" - eventemitter3 "^5.0.1" - lodash "^4.17.21" - lru-cache "^10.2.0" - pino "^8.19.0" - pino-pretty "^11.0.0" - rxjs "^7.8.1" - zod "^3.22.4" - -"@acala-network/chopsticks-db@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-db/-/chopsticks-db-0.10.0.tgz#4088c255fe7409d8f098e2582bc68b628a58dd3f" - integrity sha512-wVqSjOQGMMdQVfsGhLqw7PzVYL8OIgtxZYRDT1fqwd844DeEV4RzhQvIoyzzmSpMR91RHr0qTg58zoBdv7UKbQ== - dependencies: - "@acala-network/chopsticks-core" "0.10.0" + "@acala-network/chopsticks-core" "0.12.2" "@polkadot/util" "^12.6.2" idb "^8.0.0" sqlite3 "^5.1.7" typeorm "^0.3.20" -"@acala-network/chopsticks-db@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-db/-/chopsticks-db-0.10.1.tgz#30e630e23d54f97b0fe06812e242ce0ae95541ac" - integrity sha512-5zkQ0pZHEKjqYb2aQvrzPNzju0iQhe+C1QflW21m8pC0BzCXPRQGC3edyPn6Lmthq/8nLN3CJyb4LccY2ENhFQ== - dependencies: - "@acala-network/chopsticks-core" "0.10.1" - "@polkadot/util" "^12.6.2" - idb "^8.0.0" - sqlite3 "^5.1.7" - typeorm "^0.3.20" - -"@acala-network/chopsticks-executor@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-executor/-/chopsticks-executor-0.10.0.tgz#2d5e0fd179d6fa2679aaf9fc31312fa4c58a77e8" - integrity sha512-hBjlVjdCZ+d8Oo50X9U+wHqf0jQ9AucFj2dX9rLV+AQrNDOAeWob+kSqMMRcEUembg3Fyv7zIPv8OGVFrOreiQ== - dependencies: - "@polkadot/util" "^12.6.2" - "@polkadot/wasm-util" "^7.3.2" - -"@acala-network/chopsticks-executor@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-executor/-/chopsticks-executor-0.10.1.tgz#017eba0618c7e379810c4f20e90eb583df3b15c2" - integrity sha512-NUFD2GaGWIpPtE3C7i5T1BJJwMLZgQZdHgGSREQV8KndzwiuWqkxDu33N/cmDkrFmZG84S6xVnf6qPHIsw5UNQ== +"@acala-network/chopsticks-executor@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-executor/-/chopsticks-executor-0.12.2.tgz#135cf0dd014906a181a2b93d53b6fd8073e4ff30" + integrity sha512-Xby/ixWCXj5lDtp/tPbnyMX//qnzbO/jLzna/zcBzprplE/l4UGd+VX8OTV2dXTBnaLIoLP1kPTLB0L7K0tI3Q== dependencies: "@polkadot/util" "^12.6.2" "@polkadot/wasm-util" "^7.3.2" -"@acala-network/chopsticks-testing@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-testing/-/chopsticks-testing-0.10.1.tgz#dc33561df5e76bc9580b4fc851a6df9f831fedb0" - integrity sha512-GaTZVckSZVLZJC7V8TmiDt3fWsSzWtg9az3/GBz1jB2wx7EJDKd9Siu2lVMyJW6jZlEGXijtuexzkOXtttAWCw== +"@acala-network/chopsticks-testing@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-testing/-/chopsticks-testing-0.12.2.tgz#f3d02e53bf477da1b663092df0cc04c8ab8f62ac" + integrity sha512-S4ef/WuUbLN/FKLOoLRXsc0pNYtES9CU91fPGyJ7nYEb15Z/kcdy4JQI9QSwUGRkHX4U0SY6XkI232Hpi8ijAw== dependencies: - "@acala-network/chopsticks-utils" "0.10.1" - "@polkadot/api" "^10.11.2" - "@polkadot/types" "^10.11.2" + "@acala-network/chopsticks-utils" "0.12.2" + "@polkadot/api" "^11.2.1" + "@polkadot/types" "^11.2.1" -"@acala-network/chopsticks-utils@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-utils/-/chopsticks-utils-0.10.1.tgz#5839afc2c5f14b7db029116432f1b870370803d7" - integrity sha512-P7V5B5SbWqgplqdpExjsxbFW2S/+fA2hQ0Ow9o57SsM0qg3sH3YaVd2jBJ6kEHXGoFxUGqUIoiKm/+3cYUUImA== +"@acala-network/chopsticks-utils@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@acala-network/chopsticks-utils/-/chopsticks-utils-0.12.2.tgz#e3fe3c22677d1f1ab5d4a60b0d2742c1ee8e2204" + integrity sha512-hdmjYS+FEJ4sVnP72PYz3QM5qU53wiT+uiddqNX1QdC0xyStEm7KFzfIp/hfaWtqp3L1Sn3yJ9i3X7sjED1IzQ== dependencies: - "@acala-network/chopsticks" "0.10.1" - "@polkadot/api" "^10.11.2" - "@polkadot/types" "^10.11.2" - -"@acala-network/chopsticks@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks/-/chopsticks-0.10.0.tgz#d4af5b220b8b14b79254ebd0622c2f500828c25b" - integrity sha512-mx5hIGOHll0tz1+jRA4jpC+GaUhLeTWULSFyf9DgbHHR0rfxtzhNgML8EOIrIA5Otg3v0/0WlBwFqvMRQCvFwA== - dependencies: - "@acala-network/chopsticks-core" "0.10.0" - "@acala-network/chopsticks-db" "0.10.0" - "@pnpm/npm-conf" "^2.2.2" - "@polkadot/api-augment" "^10.11.2" - "@polkadot/types" "^10.11.2" + "@acala-network/chopsticks" "0.12.2" + "@polkadot/api" "^11.2.1" + "@polkadot/api-base" "^11.2.1" + "@polkadot/keyring" "^12.6.2" + "@polkadot/types" "^11.2.1" "@polkadot/util" "^12.6.2" - "@polkadot/util-crypto" "^12.6.2" - axios "^1.6.8" - dotenv "^16.4.5" - global-agent "^3.0.0" - js-yaml "^4.1.0" - jsondiffpatch "^0.5.0" - lodash "^4.17.21" - ws "^8.16.0" - yargs "^17.7.2" - zod "^3.22.4" -"@acala-network/chopsticks@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@acala-network/chopsticks/-/chopsticks-0.10.1.tgz#466a04328a580c077ecd8544a2d2939c5a5f2817" - integrity sha512-zSMK8G3wbrygKQJUjftotXBqlDHpTAZHNgZdyso8wGX+mKjEvtzVPt5ADX/h9pDXgvg5yehyJHTjQBL6QbwDdw== +"@acala-network/chopsticks@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@acala-network/chopsticks/-/chopsticks-0.12.2.tgz#0b11d6fbe8502ee9c6eb7901eea17686cf2157ee" + integrity sha512-/4D1CsBF8aKEy6DKMvdGAmE3+DzdNlXtg+Qc+W97Lyv/m68fv60HiOi0E5I3+jV4fq+VSuLKHGrMyJJh2/sRRg== dependencies: - "@acala-network/chopsticks-core" "0.10.1" - "@acala-network/chopsticks-db" "0.10.1" + "@acala-network/chopsticks-core" "0.12.2" + "@acala-network/chopsticks-db" "0.12.2" "@pnpm/npm-conf" "^2.2.2" - "@polkadot/api-augment" "^10.11.2" - "@polkadot/types" "^10.11.2" + "@polkadot/api" "^11.2.1" + "@polkadot/api-augment" "^11.2.1" + "@polkadot/rpc-provider" "^11.2.1" + "@polkadot/types" "^11.2.1" "@polkadot/util" "^12.6.2" "@polkadot/util-crypto" "^12.6.2" axios "^1.6.8" + comlink "^4.4.1" dotenv "^16.4.5" global-agent "^3.0.0" js-yaml "^4.1.0" @@ -576,16 +520,34 @@ "@polkadot-api/substrate-client" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" "@polkadot-api/utils" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" +"@polkadot-api/json-rpc-provider-proxy@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.0.1.tgz#bb5c943642cdf0ec7bc48c0a2647558b9fcd7bdb" + integrity sha512-gmVDUP8LpCH0BXewbzqXF2sdHddq1H1q+XrAW2of+KZj4woQkIGBRGTJHeBEVHe30EB+UejR1N2dT4PO/RvDdg== + "@polkadot-api/json-rpc-provider-proxy@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" resolved "https://registry.yarnpkg.com/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#cc28fb801db6a47824261a709ab924ec6951eb96" integrity sha512-0hZ8vtjcsyCX8AyqP2sqUHa1TFFfxGWmlXJkit0Nqp9b32MwZqn5eaUAiV2rNuEpoglKOdKnkGtUF8t5MoodKw== +"@polkadot-api/json-rpc-provider@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1.tgz#333645d40ccd9bccfd1f32503f17e4e63e76e297" + integrity sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA== + "@polkadot-api/json-rpc-provider@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" resolved "https://registry.yarnpkg.com/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#2f71bfb192d28dd4c400ef8b1c5f934c676950f3" integrity sha512-EaUS9Fc3wsiUr6ZS43PQqaRScW7kM6DYbuM/ou0aYjm8N9MBqgDbGm2oL6RE1vAVmOfEuHcXZuZkhzWtyvQUtA== +"@polkadot-api/metadata-builders@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@polkadot-api/metadata-builders/-/metadata-builders-0.0.1.tgz#a76b48febef9ea72be8273d889e2677101045a05" + integrity sha512-GCI78BHDzXAF/L2pZD6Aod/yl82adqQ7ftNmKg51ixRL02JpWUA+SpUKTJE5MY1p8kiJJIo09P2um24SiJHxNA== + dependencies: + "@polkadot-api/substrate-bindings" "0.0.1" + "@polkadot-api/utils" "0.0.1" + "@polkadot-api/metadata-builders@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" resolved "https://registry.yarnpkg.com/@polkadot-api/metadata-builders/-/metadata-builders-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#085db2a3c7b100626b2fae3be35a32a24ea7714f" @@ -594,6 +556,26 @@ "@polkadot-api/substrate-bindings" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" "@polkadot-api/utils" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" +"@polkadot-api/observable-client@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@polkadot-api/observable-client/-/observable-client-0.1.0.tgz#472045ea06a2bc4bccdc2db5c063eadcbf6f5351" + integrity sha512-GBCGDRztKorTLna/unjl/9SWZcRmvV58o9jwU2Y038VuPXZcr01jcw/1O3x+yeAuwyGzbucI/mLTDa1QoEml3A== + dependencies: + "@polkadot-api/metadata-builders" "0.0.1" + "@polkadot-api/substrate-bindings" "0.0.1" + "@polkadot-api/substrate-client" "0.0.1" + "@polkadot-api/utils" "0.0.1" + +"@polkadot-api/substrate-bindings@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-bindings/-/substrate-bindings-0.0.1.tgz#c4b7f4d6c3672d2c15cbc6c02964f014b73cbb0b" + integrity sha512-bAe7a5bOPnuFVmpv7y4BBMRpNTnMmE0jtTqRUw/+D8ZlEHNVEJQGr4wu3QQCl7k1GnSV1wfv3mzIbYjErEBocg== + dependencies: + "@noble/hashes" "^1.3.1" + "@polkadot-api/utils" "0.0.1" + "@scure/base" "^1.1.1" + scale-ts "^1.6.0" + "@polkadot-api/substrate-bindings@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-bindings/-/substrate-bindings-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#f836a554a9ead6fb6356079c725cd53f87238932" @@ -604,17 +586,27 @@ "@scure/base" "^1.1.1" scale-ts "^1.6.0" +"@polkadot-api/substrate-client@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-client/-/substrate-client-0.0.1.tgz#0e8010a0abe2fb47d6fa7ab94e45e1d0de083314" + integrity sha512-9Bg9SGc3AwE+wXONQoW8GC00N3v6lCZLW74HQzqB6ROdcm5VAHM4CB/xRzWSUF9CXL78ugiwtHx3wBcpx4H4Wg== + "@polkadot-api/substrate-client@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-client/-/substrate-client-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#55ae463f4143495e328465dd16b03e71663ef4c4" integrity sha512-lcdvd2ssUmB1CPzF8s2dnNOqbrDa+nxaaGbuts+Vo8yjgSKwds2Lo7Oq+imZN4VKW7t9+uaVcKFLMF7PdH0RWw== +"@polkadot-api/utils@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@polkadot-api/utils/-/utils-0.0.1.tgz#908b22becac705149d7cc946532143d0fb003bfc" + integrity sha512-3j+pRmlF9SgiYDabSdZsBSsN5XHbpXOAce1lWj56IEEaFZVjsiCaxDOA7C9nCcgfVXuvnbxqqEGQvnY+QfBAUw== + "@polkadot-api/utils@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" resolved "https://registry.yarnpkg.com/@polkadot-api/utils/-/utils-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#759698dcf948745ea37cc5ab6abd49a00f1b0c31" integrity sha512-0CYaCjfLQJTCRCiYvZ81OncHXEKPzAexCMoVloR+v2nl/O2JRya/361MtPkeNLC6XBoaEgLAG9pWQpH3WePzsw== -"@polkadot/api-augment@10.12.4", "@polkadot/api-augment@^10.11.2": +"@polkadot/api-augment@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/api-augment/-/api-augment-10.12.4.tgz#cbf03b97d60c204d05cb2478e6f862e68877cd71" integrity sha512-ZKKeA8OnB1kkqBlvMhTw7QpPsRxpf/OE2UGhuCabQmU+MysYaWTPGssqAFEBqUZ+iyvTk83s+ssoMlXxdIhblA== @@ -627,6 +619,19 @@ "@polkadot/util" "^12.6.2" tslib "^2.6.2" +"@polkadot/api-augment@11.3.1", "@polkadot/api-augment@^11.2.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-augment/-/api-augment-11.3.1.tgz#c7deeac438b017613e244c25505216a9d4c3977e" + integrity sha512-Yj+6rb6h0WwY3yJ+UGhjGW+tyMRFUMsKQuGw+eFsXdjiNU9UoXsAqA2dG7Q1F+oeX/g+y2gLGBezNoCwbl6HfA== + dependencies: + "@polkadot/api-base" "11.3.1" + "@polkadot/rpc-augment" "11.3.1" + "@polkadot/types" "11.3.1" + "@polkadot/types-augment" "11.3.1" + "@polkadot/types-codec" "11.3.1" + "@polkadot/util" "^12.6.2" + tslib "^2.6.2" + "@polkadot/api-base@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/api-base/-/api-base-10.12.4.tgz#a4bcb69097e177ad2a2e67e89857244f34931e0b" @@ -638,6 +643,17 @@ rxjs "^7.8.1" tslib "^2.6.2" +"@polkadot/api-base@11.3.1", "@polkadot/api-base@^11.2.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-base/-/api-base-11.3.1.tgz#6c74c88d4a4b3d22344bb8715a135493f5a3dd33" + integrity sha512-b8UkNL00NN7+3QaLCwL5cKg+7YchHoKCAhwKusWHNBZkkO6Oo2BWilu0dZkPJOyqV9P389Kbd9+oH+SKs9u2VQ== + dependencies: + "@polkadot/rpc-core" "11.3.1" + "@polkadot/types" "11.3.1" + "@polkadot/util" "^12.6.2" + rxjs "^7.8.1" + tslib "^2.6.2" + "@polkadot/api-derive@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-10.12.4.tgz#a3b4cc3eb685dd3255d36e534f6810af668a5b45" @@ -654,6 +670,22 @@ rxjs "^7.8.1" tslib "^2.6.2" +"@polkadot/api-derive@11.3.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-11.3.1.tgz#3617655b6dab56d5beb8efbf7383ab457370df35" + integrity sha512-9dopzrh4cRuft1nANmBvMY/hEhFDu0VICMTOGxQLOl8NMfcOFPTLAN0JhSBUoicGZhV+c4vpv01NBx/7/IL1HA== + dependencies: + "@polkadot/api" "11.3.1" + "@polkadot/api-augment" "11.3.1" + "@polkadot/api-base" "11.3.1" + "@polkadot/rpc-core" "11.3.1" + "@polkadot/types" "11.3.1" + "@polkadot/types-codec" "11.3.1" + "@polkadot/util" "^12.6.2" + "@polkadot/util-crypto" "^12.6.2" + rxjs "^7.8.1" + tslib "^2.6.2" + "@polkadot/api@10.12.4", "@polkadot/api@^10.11.2": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-10.12.4.tgz#eeec23a45a26d173b5717e1b840d2d2fb19a9fc7" @@ -677,6 +709,29 @@ rxjs "^7.8.1" tslib "^2.6.2" +"@polkadot/api@11.3.1", "@polkadot/api@^11.2.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-11.3.1.tgz#6092aea8147ea03873b3f383cceae0390a67f71d" + integrity sha512-q4kFIIHTLvKxM24b0Eo8hJevsPMme+aITJGrDML9BgdZYTRN14+cu5nXiCsQvaEamdyYj+uCXWe2OV9X7pPxsA== + dependencies: + "@polkadot/api-augment" "11.3.1" + "@polkadot/api-base" "11.3.1" + "@polkadot/api-derive" "11.3.1" + "@polkadot/keyring" "^12.6.2" + "@polkadot/rpc-augment" "11.3.1" + "@polkadot/rpc-core" "11.3.1" + "@polkadot/rpc-provider" "11.3.1" + "@polkadot/types" "11.3.1" + "@polkadot/types-augment" "11.3.1" + "@polkadot/types-codec" "11.3.1" + "@polkadot/types-create" "11.3.1" + "@polkadot/types-known" "11.3.1" + "@polkadot/util" "^12.6.2" + "@polkadot/util-crypto" "^12.6.2" + eventemitter3 "^5.0.1" + rxjs "^7.8.1" + tslib "^2.6.2" + "@polkadot/keyring@^12.6.2": version "12.6.2" resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.6.2.tgz#6067e6294fee23728b008ac116e7e9db05cecb9b" @@ -706,6 +761,17 @@ "@polkadot/util" "^12.6.2" tslib "^2.6.2" +"@polkadot/rpc-augment@11.3.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-11.3.1.tgz#b589ef5b7ab578cf274077604543071ce9889301" + integrity sha512-2PaDcKNju4QYQpxwVkWbRU3M0t340nMX9cMo+8awgvgL1LliV/fUDZueMKLuSS910JJMTPQ7y2pK4eQgMt08gQ== + dependencies: + "@polkadot/rpc-core" "11.3.1" + "@polkadot/types" "11.3.1" + "@polkadot/types-codec" "11.3.1" + "@polkadot/util" "^12.6.2" + tslib "^2.6.2" + "@polkadot/rpc-core@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-10.12.4.tgz#ecb5500335e6e202d0551356928289725321cf7c" @@ -718,7 +784,19 @@ rxjs "^7.8.1" tslib "^2.6.2" -"@polkadot/rpc-provider@10.12.4", "@polkadot/rpc-provider@^10.11.2": +"@polkadot/rpc-core@11.3.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-11.3.1.tgz#c23e23ee5c403c4edb207d603ae4dc16e69bc710" + integrity sha512-KKNepsDd/mpmXcA6v/h14eFFPEzLGd7nrvx2UUXUxoZ0Fq2MH1hplP3s93k1oduNY/vOXJR2K9S4dKManA6GVQ== + dependencies: + "@polkadot/rpc-augment" "11.3.1" + "@polkadot/rpc-provider" "11.3.1" + "@polkadot/types" "11.3.1" + "@polkadot/util" "^12.6.2" + rxjs "^7.8.1" + tslib "^2.6.2" + +"@polkadot/rpc-provider@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-10.12.4.tgz#91fc29064e9d8152d8fec8fbf401b5be7ab3716e" integrity sha512-awXLK28nt6BvOwoTnOVPtz+Qu5sx40Al1yb5lzKG6jYFQrEmqrENufHNOCLU3Uspfqmc6eJmNluZOmVtJKDCPg== @@ -738,6 +816,26 @@ optionalDependencies: "@substrate/connect" "0.8.8" +"@polkadot/rpc-provider@11.3.1", "@polkadot/rpc-provider@^11.2.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-11.3.1.tgz#1d1289bf42d065b5f04f9baa46ef90d96940819e" + integrity sha512-pqERChoHo45hd3WAgW8UuzarRF+G/o/eXEbl0PXLubiayw4X4qCmIzmtntUcKYgxGNcYGZaG87ZU8OjN97m6UA== + dependencies: + "@polkadot/keyring" "^12.6.2" + "@polkadot/types" "11.3.1" + "@polkadot/types-support" "11.3.1" + "@polkadot/util" "^12.6.2" + "@polkadot/util-crypto" "^12.6.2" + "@polkadot/x-fetch" "^12.6.2" + "@polkadot/x-global" "^12.6.2" + "@polkadot/x-ws" "^12.6.2" + eventemitter3 "^5.0.1" + mock-socket "^9.3.1" + nock "^13.5.0" + tslib "^2.6.2" + optionalDependencies: + "@substrate/connect" "0.8.10" + "@polkadot/types-augment@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/types-augment/-/types-augment-10.12.4.tgz#e396b9a0a9bf428a4352cbb36ecf3a5ebc696e19" @@ -748,7 +846,17 @@ "@polkadot/util" "^12.6.2" tslib "^2.6.2" -"@polkadot/types-codec@10.12.4", "@polkadot/types-codec@^10.11.2": +"@polkadot/types-augment@11.3.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-augment/-/types-augment-11.3.1.tgz#1f7f553f0ca6eb8fbc0306901edc045fe18729e1" + integrity sha512-eR3HVpvUmB3v7q2jTWVmVfAVfb1/kuNn7ij94Zqadg/fuUq0pKqIOKwkUj3OxRM3A/5BnW3MbgparjKD3r+fyw== + dependencies: + "@polkadot/types" "11.3.1" + "@polkadot/types-codec" "11.3.1" + "@polkadot/util" "^12.6.2" + tslib "^2.6.2" + +"@polkadot/types-codec@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/types-codec/-/types-codec-10.12.4.tgz#21052005bb448814d0d16230c1835a81f601e450" integrity sha512-8SEwgQT+JfmI62C9MZisA/1oQFuQW1OySvZFZlSqkaoRooK+JMl7Sp9fnRhCuiHMiz08YO4lX16O+aAu0/bmmw== @@ -757,6 +865,15 @@ "@polkadot/x-bigint" "^12.6.2" tslib "^2.6.2" +"@polkadot/types-codec@11.3.1", "@polkadot/types-codec@^11.2.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-codec/-/types-codec-11.3.1.tgz#2767cf482cd49afdd5dce9701615f68ec59cec5e" + integrity sha512-i7IiiuuL+Z/jFoKTA9xeh4wGQnhnNNjMT0+1ohvlOvnFsoKZKFQQOaDPPntGJVL1JDCV+KjkN2uQKZSeW8tguQ== + dependencies: + "@polkadot/util" "^12.6.2" + "@polkadot/x-bigint" "^12.6.2" + tslib "^2.6.2" + "@polkadot/types-create@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/types-create/-/types-create-10.12.4.tgz#f271a9625ab9d533f41e10ad1ffac7a9d7a910f1" @@ -766,7 +883,16 @@ "@polkadot/util" "^12.6.2" tslib "^2.6.2" -"@polkadot/types-known@10.12.4", "@polkadot/types-known@^10.11.2": +"@polkadot/types-create@11.3.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-create/-/types-create-11.3.1.tgz#3ac2c8283f61555f9e572ca30e3485b95a0a54e2" + integrity sha512-pBXtpz5FehcRJ6j5MzFUIUN8ZWM7z6HbqK1GxBmYbJVRElcGcOg7a/rL2pQVphU0Rx1E8bSO4thzGf4wUxSX7w== + dependencies: + "@polkadot/types-codec" "11.3.1" + "@polkadot/util" "^12.6.2" + tslib "^2.6.2" + +"@polkadot/types-known@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-10.12.4.tgz#5a2c002a0af405a11f11ee93c3b2bb95788db03d" integrity sha512-fiS26ep9QwHIUn/N0X9R3DIFP8ar4cEG/oJyxs5uBNtIEiAZdWucEZAZFxJnNp6Lib0PGYaz9T9ph0+UbnKKEg== @@ -778,6 +904,18 @@ "@polkadot/util" "^12.6.2" tslib "^2.6.2" +"@polkadot/types-known@11.3.1", "@polkadot/types-known@^11.2.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-11.3.1.tgz#fc34ed29ac2474db6b66805a15d12008226346bb" + integrity sha512-3BIof7u6tn9bk3ZCIxA07iNoQ3uj4+vn3DTOjCKECozkRlt6V+kWRvqh16Hc0SHMg/QjcMb2fIu/WZhka1McUQ== + dependencies: + "@polkadot/networks" "^12.6.2" + "@polkadot/types" "11.3.1" + "@polkadot/types-codec" "11.3.1" + "@polkadot/types-create" "11.3.1" + "@polkadot/util" "^12.6.2" + tslib "^2.6.2" + "@polkadot/types-support@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/types-support/-/types-support-10.12.4.tgz#e59db00e2d7665d2a32d5f8c18d548ad2b10e1bd" @@ -786,7 +924,15 @@ "@polkadot/util" "^12.6.2" tslib "^2.6.2" -"@polkadot/types@10.12.4", "@polkadot/types@^10.11.2": +"@polkadot/types-support@11.3.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-support/-/types-support-11.3.1.tgz#dee02a67784baa13177fe9957f5d8d62e8a7e570" + integrity sha512-jTFz1GKyF7nI29yIOq4v0NiWTOf5yX4HahJNeFD8TcxoLhF+6tH/XXqrUXJEfbaTlSrRWiW1LZYlb+snctqKHA== + dependencies: + "@polkadot/util" "^12.6.2" + tslib "^2.6.2" + +"@polkadot/types@10.12.4": version "10.12.4" resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-10.12.4.tgz#6c26c81fd523c1b75e53eccf676f3d8697358699" integrity sha512-KJfxAdOyA/ZmGzRpRWojZx6hOU4iFHiwmerAZQzxELMCUCSsAd4joiXWQX7leSrlJCvvk8/VecnXGTqRe8jtGw== @@ -800,6 +946,20 @@ rxjs "^7.8.1" tslib "^2.6.2" +"@polkadot/types@11.3.1", "@polkadot/types@^11.2.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-11.3.1.tgz#bab61b701218158099e3f548d20efc27cbf1287f" + integrity sha512-5c7uRFXQTT11Awi6T0yFIdAfD6xGDAOz06Kp7M5S9OGNZY28wSPk5x6BYfNphWPaIBmHHewYJB5qmnrdYQAWKQ== + dependencies: + "@polkadot/keyring" "^12.6.2" + "@polkadot/types-augment" "11.3.1" + "@polkadot/types-codec" "11.3.1" + "@polkadot/types-create" "11.3.1" + "@polkadot/util" "^12.6.2" + "@polkadot/util-crypto" "^12.6.2" + rxjs "^7.8.1" + tslib "^2.6.2" + "@polkadot/util-crypto@12.6.2", "@polkadot/util-crypto@^12.6.2": version "12.6.2" resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.2.tgz#d2d51010e8e8ca88951b7d864add797dad18bbfc" @@ -1005,9 +1165,9 @@ integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw== "@scure/base@^1.1.1", "@scure/base@^1.1.5": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" - integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== + version "1.1.7" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" + integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -1025,9 +1185,24 @@ integrity sha512-nKu8pDrE3LNCEgJjZe1iGXzaD6OSIDD4Xzz/yo4KO9mQ6LBvf49BVrt4qxBFGL6++NneLiWUZGoh+VSd4PyVIg== "@substrate/connect-known-chains@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@substrate/connect-known-chains/-/connect-known-chains-1.1.2.tgz#987952dd46a87485285b4fd74a3d3051efe90181" - integrity sha512-XvyemTVqon+6EF2G7QL0fEXxjuz3nUNFgFV0TSWhSVpPb+Sfs+vfipbEZxGNouxvjCoJdr6CF0rwgGsrrKOnAA== + version "1.1.6" + resolved "https://registry.yarnpkg.com/@substrate/connect-known-chains/-/connect-known-chains-1.1.6.tgz#2627d329b82b46c7d745752c48c73e1b8ce6ac81" + integrity sha512-JwtdGbnK3ZqrY1qp3Ifr/p648sp9hG0Q715h4nRghnqZJnMQIiLKaFkcLnvrAiYQD3zNTYDztHidy5Q/u0TcbQ== + +"@substrate/connect-known-chains@^1.1.4": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@substrate/connect-known-chains/-/connect-known-chains-1.1.8.tgz#ab54100c9fd102212b819231477393adc435e83a" + integrity sha512-W0Cpnk//LoMTu5BGDCRRg5NHFR2aZ9OJtLGSgRyq1RP39dQGpoVZIgNC6z+SWRzlmOz3gIgkUCwGvOKVt2BabA== + +"@substrate/connect@0.8.10": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@substrate/connect/-/connect-0.8.10.tgz#810b6589f848828aa840c731a1f36b84fe0e5956" + integrity sha512-DIyQ13DDlXqVFnLV+S6/JDgiGowVRRrh18kahieJxhgvzcWicw5eLc6jpfQ0moVVLBYkO7rctB5Wreldwpva8w== + dependencies: + "@substrate/connect-extension-protocol" "^2.0.0" + "@substrate/connect-known-chains" "^1.1.4" + "@substrate/light-client-extension-helpers" "^0.0.6" + smoldot "2.0.22" "@substrate/connect@0.8.8": version "0.8.8" @@ -1052,10 +1227,23 @@ "@substrate/connect-known-chains" "^1.1.1" rxjs "^7.8.1" +"@substrate/light-client-extension-helpers@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-0.0.6.tgz#bec1c7997241226db50b44ad85a992b4348d21c3" + integrity sha512-girltEuxQ1BvkJWmc8JJlk4ZxnlGXc/wkLcNguhY+UoDEMBK0LsdtfzQKIfrIehi4QdeSBlFEFBoI4RqPmsZzA== + dependencies: + "@polkadot-api/json-rpc-provider" "0.0.1" + "@polkadot-api/json-rpc-provider-proxy" "0.0.1" + "@polkadot-api/observable-client" "0.1.0" + "@polkadot-api/substrate-client" "0.0.1" + "@substrate/connect-extension-protocol" "^2.0.0" + "@substrate/connect-known-chains" "^1.1.4" + rxjs "^7.8.1" + "@substrate/ss58-registry@^1.44.0": - version "1.47.0" - resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.47.0.tgz#99b11fd3c16657f5eae483b3df7c545ca756d1fc" - integrity sha512-6kuIJedRcisUJS2pgksEH2jZf3hfSIVzqtFzs/AyjTW3ETbMg5q1Bb7VWa0WYaT6dTrEXp/6UoXM5B9pSIUmcw== + version "1.48.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.48.0.tgz#b50b577b491274dbab55711d2e933456637e73d0" + integrity sha512-lE9TGgtd93fTEIoHhSdtvSFBoCsvTbqiCvQIMvX4m6BO/hESywzzTzTFMVP1doBwDDMAN4lsMfIM3X3pdmt7kQ== "@tootallnate/once@1": version "1.1.2" @@ -1104,7 +1292,14 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/node@*", "@types/node@^20.11.30": +"@types/node@*": + version "20.14.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.7.tgz#342cada27f97509eb8eb2dbc003edf21ce8ab5a8" + integrity sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ== + dependencies: + undici-types "~5.26.4" + +"@types/node@^20.11.30": version "20.11.30" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== @@ -1527,9 +1722,9 @@ axe-core@=4.7.0: integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== axios@^1.6.8: - version "1.6.8" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" - integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -1868,14 +2063,14 @@ dateformat@^4.6.3: integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== dayjs@^1.11.9: - version "1.11.10" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" - integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + version "1.11.11" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" + integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== -debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@4, debug@^4.1.0, debug@^4.3.3, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== dependencies: ms "2.1.2" @@ -1886,6 +2081,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -2550,7 +2752,7 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -fast-copy@^3.0.0: +fast-copy@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35" integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ== @@ -2665,9 +2867,9 @@ for-each@^0.3.3: is-callable "^1.1.3" foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== dependencies: cross-spawn "^7.0.0" signal-exit "^4.0.1" @@ -2806,15 +3008,16 @@ glob-parent@^6.0.2: is-glob "^4.0.3" glob@^10.3.10: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + version "10.4.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" + integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== dependencies: foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" glob@^7.1.3, glob@^7.1.4: version "7.2.3" @@ -2847,7 +3050,15 @@ globals@^13.19.0, globals@^13.24.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.1, globalthis@^1.0.3: +globalthis@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +globalthis@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== @@ -3271,10 +3482,10 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" -jackspeak@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== +jackspeak@^3.1.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" + integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: @@ -3411,10 +3622,10 @@ loupe@^2.3.6, loupe@^2.3.7: dependencies: get-func-name "^2.0.1" -lru-cache@^10.2.0, "lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== lru-cache@^6.0.0: version "6.0.0" @@ -3504,7 +3715,7 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@9.0.3, minimatch@^9.0.1: +minimatch@9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== @@ -3581,10 +3792,10 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" - integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" @@ -3673,9 +3884,9 @@ nock@^13.5.0: propagate "^2.0.0" node-abi@^3.3.0: - version "3.56.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.56.0.tgz#ca807d5ff735ac6bbbd684ae3ff2debc1c2a40a7" - integrity sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q== + version "3.65.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.65.0.tgz#ca92d559388e1e9cab1680a18c1a18757cdac9d3" + integrity sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA== dependencies: semver "^7.3.5" @@ -3859,6 +4070,11 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -3908,12 +4124,12 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-type@^4.0.0: @@ -3941,22 +4157,22 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz#083d98f966262164504afb989bccd05f665937a8" - integrity sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA== +pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== dependencies: readable-stream "^4.0.0" split2 "^4.0.0" pino-pretty@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.0.0.tgz#9b883f7b933f58fa94caa44225aab302409461ef" - integrity sha512-YFJZqw59mHIY72wBnBs7XhLGG6qpJMa4pEQTRgEPEbjIYbng2LXEZZF1DoyDg9CfejEy8uZCyzpcBXXG0oOCwQ== + version "11.2.1" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.2.1.tgz#de9a42ff8ea7b26da93506bb9e49d0b566c5ae96" + integrity sha512-O05NuD9tkRasFRWVaF/uHLOvoRDFD7tb5VMertr78rbsYFjYp48Vg3477EshVAF5eZaEw+OpDl/tu+B0R5o+7g== dependencies: colorette "^2.0.7" dateformat "^4.6.3" - fast-copy "^3.0.0" + fast-copy "^3.0.2" fast-safe-stringify "^2.1.1" help-me "^5.0.0" joycon "^3.1.1" @@ -3966,7 +4182,7 @@ pino-pretty@^11.0.0: pump "^3.0.0" readable-stream "^4.0.0" secure-json-parse "^2.4.0" - sonic-boom "^3.0.0" + sonic-boom "^4.0.1" strip-json-comments "^3.1.1" pino-std-serializers@^6.0.0: @@ -3975,21 +4191,21 @@ pino-std-serializers@^6.0.0: integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== pino@^8.19.0: - version "8.19.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.19.0.tgz#ccc15ef736f103ec02cfbead0912bc436dc92ce4" - integrity sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA== + version "8.21.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== dependencies: atomic-sleep "^1.0.0" fast-redact "^3.1.1" on-exit-leak-free "^2.1.0" - pino-abstract-transport v1.1.0 + pino-abstract-transport "^1.2.0" pino-std-serializers "^6.0.0" process-warning "^3.0.0" quick-format-unescaped "^4.0.3" real-require "^0.2.0" safe-stable-stringify "^2.3.1" sonic-boom "^3.7.0" - thread-stream "^2.0.0" + thread-stream "^2.6.0" pkg-types@^1.0.3: version "1.0.3" @@ -4160,9 +4376,9 @@ real-require@^0.2.0: integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== reflect-metadata@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.1.tgz#8d5513c0f5ef2b4b9c3865287f3c0940c1f67f74" - integrity sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw== + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== reflect.getprototypeof@^1.0.4: version "1.0.6" @@ -4335,13 +4551,18 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: +semver@^7.0.0, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" +semver@^7.3.2, semver@^7.3.5: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" @@ -4462,17 +4683,24 @@ socks-proxy-agent@^6.0.0: socks "^2.6.2" socks@^2.6.2: - version "2.8.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.1.tgz#22c7d9dd7882649043cba0eafb49ae144e3457af" - integrity sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ== + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: ip-address "^9.0.5" smart-buffer "^4.2.0" -sonic-boom@^3.0.0, sonic-boom@^3.7.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.0.tgz#e442c5c23165df897d77c3c14ef3ca40dec66a66" - integrity sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA== +sonic-boom@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== + dependencies: + atomic-sleep "^1.0.0" + +sonic-boom@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" + integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ== dependencies: atomic-sleep "^1.0.0" @@ -4520,7 +4748,16 @@ std-env@^3.5.0: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4573,7 +4810,14 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -4686,10 +4930,10 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" -thread-stream@^2.0.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.4.1.tgz#6d588b14f0546e59d3f306614f044bc01ce43351" - integrity sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg== +thread-stream@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== dependencies: real-require "^0.2.0" @@ -4750,9 +4994,9 @@ tsconfig-paths@^3.15.0: strip-bom "^3.0.0" tslib@^2.1.0, tslib@^2.5.0, tslib@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tsx@^4.7.1: version "4.7.1" @@ -5044,7 +5288,16 @@ wide-align@^1.1.5: dependencies: string-width "^1.0.2 || 2 || 3 || 4" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5068,9 +5321,9 @@ wrappy@1: integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== ws@^8.15.1, ws@^8.16.0, ws@^8.8.1: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" - integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== y18n@^5.0.5: version "5.0.8" @@ -5134,6 +5387,6 @@ yocto-queue@^1.0.0: integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== zod@^3.22.4: - version "3.22.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" - integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/pallets/did/src/lib.rs b/pallets/did/src/lib.rs index 52967d44c..324a20dcc 100644 --- a/pallets/did/src/lib.rs +++ b/pallets/did/src/lib.rs @@ -126,10 +126,9 @@ use frame_support::{ }; use frame_system::ensure_signed; use parity_scale_codec::Encode; -use sp_runtime::{traits::Dispatchable, DispatchError}; use sp_runtime::{ - traits::{Saturating, Zero}, - SaturatedConversion, + traits::{Dispatchable, Saturating, Zero}, + DispatchError, SaturatedConversion, }; use sp_std::{boxed::Box, fmt::Debug, prelude::Clone}; diff --git a/pallets/pallet-asset-switch/Cargo.toml b/pallets/pallet-asset-switch/Cargo.toml new file mode 100644 index 000000000..ace29bacf --- /dev/null +++ b/pallets/pallet-asset-switch/Cargo.toml @@ -0,0 +1,59 @@ +[package] +authors = { workspace = true } +description = "Pallet enabling local currency to be exchanged for a remote asset on a remote location for a 1:1 exchange ratio." +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license-file = { workspace = true } +name = "pallet-asset-switch" +readme = "README.md" +repository = { workspace = true } +version = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dev-dependencies] +env_logger = { workspace = true } +pallet-balances = { workspace = true, features = ["std"] } +sp-keystore = { workspace = true } + +[dependencies] +frame-support = { workspace = true } +frame-system = { workspace = true } +log = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +xcm = { workspace = true } +xcm-builder = { workspace = true } +xcm-executor = { workspace = true } + +# Benchmarks +frame-benchmarking = { workspace = true, optional = true } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", +] +std = [ + "frame-support/std", + "frame-system/std", + "parity-scale-codec/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "xcm-builder/std", + "xcm-executor/std", + "xcm/std", +] +try-runtime = ["frame-support/try-runtime", "frame-system/try-runtime"] diff --git a/pallets/pallet-asset-switch/README.md b/pallets/pallet-asset-switch/README.md new file mode 100644 index 000000000..ce3815c74 --- /dev/null +++ b/pallets/pallet-asset-switch/README.md @@ -0,0 +1,141 @@ +# Asset switch pallet + +The asset switch pallet allows for switching the chain local currency 1:1 with a remote asset at a remote destination, according to the provided configuration, and using XCM. + +This is possible by creating a *switch pair*, which contains information about the remote asset's identifier (e.g., its `Location`), the remote location where the asset lives (and with which XCM communication takes place), the circulating supply of the remote asset, which can be switched back for the local currency, and additional information relevant for the XCM communication, which is explained more in-depth later on. + +## Summary + +The pallet lets users on the local chain lock `N` tokens of the chain's native currency into a switch pair-specific account to receive `N` remote assets on the configured remote location, which are transferred to them from the source chain's sovereign account on the remote chain. + +## Design choices + +The pallet aims to be generic enough for most parachains to be able to add one or more instances of the asset switch pallet into their runtime, after configuring it according to their needs. + +The pallet makes the following assumptions: + +* The local asset to switch is always the chain local currency, implementing the `fungible::Mutate` trait. +* The switch ratio is pre-defined to be 1:1, i.e., one unit of local currency for one unit of remote asset. +* The pallet only exposes calls to allow local -> remote switches. The reverse is assumed to happen via XCM reserve transfer from the configured remote location, for which the pallet crate provides all the XCM components that are dynamically configured based on the switch pair information stored within each instance of this pallet. +* The sovereign account of the source chain at destination owns the remote assets in the amount that is specified when the switch pair is created. The validation of this requirement is delegated to the source chain governance in the act leading to the creation of the switch pair, as the source chain itself has currently no means of doing that. +* Similarly, the pallet has currently no way to verify that a transfer of remote tokens from the chain sovereign account to the **specified** beneficiary has been completed successfully on the remote location, hence it takes an optimistic approach given that all the verifiable preconditions for the switch are verified on the source chain, i.e., the chain from which the switch originates. Any unexpected issues in the transfer on the remote location will most likely require intervention of the source chain governance to re-balance the state and make it consistent again. + +## Add the pallet to the runtime + +Add the following line to the runtime `Cargo.toml` dependencies section: + +```toml +pallet-asset-switch = {git = "/~https://github.com/KILTprotocol/kilt-node.git", branch = "release-1.14.0"} +``` + +The asset switch pallet is available in the KILT node release 1.14.0 and later. + +## Configure the pallet + +The pallet can be added one or more times to the runtime. + +For multiple deployments of the same pallet (e.g., to bridge the local currency to different remote assets), pass runtime configuration to the pallet's `Config` trait. + +```rust,ignore +pub type SwitchPool1 = pallet_asset_switch::Instance1; +impl pallet_asset_switch::Config for Runtime { + // Config +} + +pub type SwitchPool2 = pallet_asset_switch::Instance2; +impl pallet_asset_switch::Config for Runtime { + // Config +} +``` + +If a single instance is required, then use the default instance: + +```rust,ignore +impl pallet_asset_switch::Config for Runtime { + // Config +} +``` + +## The `Config` trait + +As the pallet is generic over the runtime specifics, the `Config` trait requires the following configuration parameters passed to it: + +- `type AccountIdConverter: TryConvert`: Because the `AccountId` type can be anything in the runtime, the converter is responsible for converting such a `AccountId` into a `Junction`, which is then used for some XCM processing. +- `type AssetTransactor: TransactAsset`: This component is used when charging the extrinsic submitter with the XCM fees that the chain will pay at the remote chain. For instance, if the transfer on the remote chain will cost 0.1 DOTs, the `AssetTransactor` might deduct 0.1 DOTs from the user's previously topped up balance on the source chain (more details below). +- `type FeeOrigin: EnsureOrigin`: The origin that can update the XCM fee to be paid for the transfer on the remote chain. +- `type LocalCurrency: MutateFungible`: The chain's local currency. +- `type PauseOrigin: EnsureOrigin`: The origin that can pause a switch pair, e.g., if a vulnerability is found. +- `type RuntimeEvent: From> + IsType<::RuntimeEvent>`: The aggregate `Event` type. +- `type SubmitterOrigin: EnsureOrigin`: The origin that can call the `switch` extrinsic and perform the switch. +- `type SwitchHooks: SwitchHooks`: Any additional runtime-specific logic that can be injected both before and after local tokens are exchanged for the remote assets, and before and after the remote assets are converted into local tokens. +- `type SwitchOrigin: EnsureOrigin`: The origin that can set, resume, and delete a switch pair. +- `type WeightInfo: WeightInfo`: The computed weights of the pallet after benchmarking it. +- `type XcmRouter: SendXcm`: The component responsible for routing XCM messages to the switch pair remote location to perform the remote asset transfer from the chain's sovereign account to the specified beneficiary. + +### Benchmark-only `Config` components + +- `type BenchmarkHelper: BenchmarkHelper`: Helper trait to allow the runtime to set the ground before the benchmark logic is executed. It allows the runtime to return any of the parameters that are used in the extrinsic benchmarks, or `None` if the runtime has no special conditions to fulfil. + +## Storage + +The pallet has a single `SwitchPair` storage value that contains a `Option`. +If unset, no switch pair is configured hence no switch can happen. +When set and its status is `Running`, switches are enabled in both directions. + +## Events + +The pallet generates the following events: + +- `SwitchPairCreated`: when a new switch pair is created by the required origin, e.g., governance. +- `SwitchPairRemoved`: when a switch pair is removed by the root origin. +- `SwitchPairResumed`: when a switch pair has (re-)enabled local to remote asset switches. +- `SwitchPairPaused`: when a switch pair has been paused. +- `SwitchPairFeeUpdated`: when the XCM fee for the switch transfer has been updated. +- `LocalToRemoteSwitchExecuted`: when a switch of some local tokens for the remote asset has taken place. +- `RemoteToLocalSwitchExecuted`: when a switch of some remote assets for the local tokens has taken place. + +## Calls + +1. `pub fn set_switch_pair(origin: OriginFor, remote_asset_total_supply: u128, remote_asset_id: Box, remote_asset_circulating_supply: u128, remote_reserve_location: Box, remote_asset_ed: u128, remote_xcm_fee: Box) -> DispatchResult`: Set a new switch pair between the local currency and the specified `remote_asset_id` on the `reserve_location`. The specified `total_issuance` includes both the `circulating_supply` (i.e., the remote asset amount that the chain does not control on the `reserve_location`) and the locked supply under the control of the chain's sovereign account on the `reserve_location`. For this reason, the value of `total_issuance` must be at least as large as `circulating_supply`. It is possible for `circulating_supply` to be `0`, in which case it means this chain controls all the `total_issuance` of the remote asset, which can be obtained by locking a corresponding amount of local tokens via the `switch` call below. + + Furthermore, the pallet calculates the account that will hold the local tokens locked in exchange for remote tokens. This account is based on the pallet runtime name as returned by the `PalletInfoAccess` trait and the value of `remote_asset_id`. The generated account must already have a balance of at least `circulating_supply`, ensuring enough local tokens are locked to satisfy all requests to exchange the remote asset for local tokens. The balance of such an account can be increased with a simple transfer after obtaining the to-be-created switch pair pool account by interacting with the [asset-switch runtime API][asset-switch-runtime-api]. + + This requirement can be bypassed with the `force_set_switch_pair` call. Only `SwitchOrigin` can call this, and in most cases it will most likely be a governance-based origin such as the one provided by referenda or collectives with high privileges. +2. `pub fn force_set_switch_pair(origin: OriginFor, remote_asset_total_supply: u128, remote_asset_id: Box, remote_asset_circulating_supply: u128, remote_reserve_location: Box, remote_asset_ed: u128, remote_xcm_fee: Box) -> DispatchResult`: The same as the `set_switch_pair`, but skips the check over the switch pair pool account balance, and requires the `root` origin for the call to be dispatched. +3. `pub fn force_unset_switch_pair(origin: OriginFor) -> DispatchResult`: Forcibly remove a previously-stored switch pair. This operation can only be called by the `root` origin. + + **Any intermediate state, such as local tokens locked in the switch pair pool or remote assets that are not switchable anymore for local tokens, must be taken care of with subsequent governance operations.** +4. `pub fn pause_switch_pair(origin: OriginFor) -> DispatchResult`: Allows the `PauseOrigin` to immediately pause switches in both directions. +5. `pub fn resume_switch_pair(origin: OriginFor) -> DispatchResult`: Allows the `SwitchOrigin` to resume switches in both directions. +6. `pub fn remote_xcm_fee(origin: OriginFor, new: Box) -> DispatchResult`: Allows the `FeeOrigin` to update the required XCM fee to execute the transfer of remote asset on the reserve location from the chain's sovereign account to the beneficiary specified in the `switch` operation. + + For example, if the cost of sending an XCM message containing a `TransferAsset` instruction from the source chain to AssetHub (reserve location) changes from 0.1 DOTs to 0.2 DOTs, the fee will need to be updated accordingly to avoid transfers failing on AssetHub, leaving the whole system in an inconsistent state. Since the pallet refunds any unused assets on the reserve location to the account initiating the switch on the source chain, it is not a big issue to overestimate this value here since no funds will be burnt or unfairly taken from the user during the switch process. +7. `pub fn switch(origin: OriginFor, local_asset_amount: LocalCurrencyBalanceOf, beneficiary: Box) -> DispatchResult`: Allows the `SubmitterOrigin` to perform a switch of some local tokens for the corresponding amount of remote assets on the configured `reserve_location`. The switch will fail on the source chain if any of the following preconditions are not met: + 1. The submitter does not have enough balance to pay for the tx fees on the source chain or to cover the amount of local tokens requested. Hence, the user's local balance must be greater than or equal to the amount of tokens requested in the switch + the cost of executing the extrinsic on the source chain. + 2. No switch pair is set or the switch pair is currently not allowing switches. + 3. There are not enough locked remote assets on the `reserve_location` to cover the switch request. e.g., if the chain sovereign account on the `reserve_location` only controls `10` remote assets, users can only switch up to `10` local tokens. Once the limit is reached, someone needs to perform the reverse operation (remote -> local switch) to free up some remote tokens. + 4. The switch pair `reserve_location` is not reachable from the source chain, because the configured `XcmRouter` returns an error (e.g., there is no XCM channel between the two chains). + 5. The configured `SwitchHooks` returns an error in either the `pre-` or the `post-` switch checks. + 6. The user does not have enough assets to pay for the required remote XCM fees as specified in the switch pair info and as returned by the configured `AssetTransactor`. + +## XCM components + +Because the switch functionality relies on XCM, the pallet provides a few XCM components that should be included in a runtime to enable the whole set of interactions between the source chain and the configured remote reserve location. + +* `AccountId32ToAccountId32JunctionConverter` in [xcm::convert][xcm-convert]: provides an implementation for the pallet's `AccountIdConverter` config component, that converts local `AccountId32`s into a `AccountId32` XCM `Junction`. This works only for chains that use `AccountId32` as their overarching `AccountId` type. +* `MatchesSwitchPairXcmFeeFungibleAsset` in [xcm::match][xcm-match]: provides an implementation of the `MatchesFungibles` that returns the input `Asset` if its ID matches the XCM fee asset ID as configured in the switch pair, if present. If no switch pair is present or if the ID does not match, it returns a [XcmExecutorError::AssetNotHandled][XcmExecutorError::AssetNotHandled], which does not prevent other matchers after it to apply their matching logic. It can be used for the `AssetTransactor` property of the [XcmExecutor::Config][XcmExecutor::Config] and as the `AssetTransactor` component of this pallet in the runtime. +* `UsingComponentsForXcmFeeAsset` in [xcm::trade][xcm-trade]: provides an implementation of `WeightTrader` that allows buying weight using the XCM fee asset configured in the switch pair. That is, if the XCM fee asset is DOT, and users need to send DOTs to this chain in order to pay for XCM fees, this component lets them use those very same DOTs that are being sent to pay for the XCM fees on this chain. Any unused weight is burnt, since this chain's sovereign account already controls the whole amount on the reserve location due to the nature of reserve-based transfers. It can be used for the `Trader` property of the [XcmExecutor::Config][XcmExecutor::Config]. +* `UsingComponentsForSwitchPairRemoteAsset` in [xcm::trade][xcm-trade]: provides an implementation of `WeightTrader` that allows buying weight using the remote asset configured in the switch pair when sending it to this chain to be switched for local tokens. Any unused weight is transferred from the switch pair account to the configured `FeeDestinationAccount`, as those local tokens do not need to back any remote assets because they have been used to pay for XCM fees. It can be used for the `Trader` property of the [XcmExecutor::Config][XcmExecutor::Config]. +* `SwitchPairRemoteAssetTransactor` in [xcm::transact][xcm-transact]: provides an implementation of `TransactAsset::deposit_asset` that matches the asset to be deposited with the remote asset configured in the switch pair '(else it returns [Error::AssetNotFound][Error::AssetNotFound]) and moves as many local tokens from the switch pair account to the specified `who` destination. It also calls into the `SwitchHooks` pre- and post- checks, and generates a `RemoteToLocalSwitchExecuted` if everything is completed successfully. It can be used for the `AssetTransactor` property of the [XcmExecutor::Config][XcmExecutor::Config]. +* `IsSwitchPairXcmFeeAsset` in [xcm::transfer][xcm-transfer]: provides an implementation of `ContainsPair` that returns `true` if the given asset and sender match the stored switch pair XCM fee asset and reserve location respectively. It can be used for the `IsReserve` property of the [XcmExecutor::Config][XcmExecutor::Config]. +* `IsSwitchPairRemoteAsset` in [xcm::transfer][xcm-transfer]: provides an implementation of `ContainsPair` that returns `true` if the given asset and sender match the stored switch pair remote asset and reserve location respectively. It can be used for the `IsReserve` property of the [XcmExecutor::Config][XcmExecutor::Config]. + +[asset-switch-runtime-api]: ../../runtime-api/asset-switch/ +[xcm-convert]: ./src/xcm/convert.rs +[xcm-match]: ./src/xcm/match.rs +[XcmExecutorError::AssetNotHandled]: /~https://github.com/paritytech/polkadot-sdk/blob/33324fe01c5b1f341687cef2aa6e767f6acf40f3/polkadot/xcm/xcm-executor/src/traits/token_matching.rs#L54 +[XcmExecutor::Config]: /~https://github.com/paritytech/polkadot-sdk/blob/33324fe01c5b1f341687cef2aa6e767f6acf40f3/polkadot/xcm/xcm-executor/src/config.rs#L31 +[xcm-trade]: ./src/xcm/trade.rs +[Error::AssetNotFound]: /~https://github.com/paritytech/polkadot-sdk/blob/e5791a56dcc35e308a80985cc3b6b7f2ed1eb6ec/polkadot/xcm/src/v3/traits.rs#L68 +[xcm-transact]: ./src/xcm/transact.rs +[xcm-transfer]: ./src/xcm/transfer.rs diff --git a/pallets/pallet-asset-switch/src/benchmarking.rs b/pallets/pallet-asset-switch/src/benchmarking.rs new file mode 100644 index 000000000..0ac195831 --- /dev/null +++ b/pallets/pallet-asset-switch/src/benchmarking.rs @@ -0,0 +1,321 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_benchmarking::v2::instance_benchmarks; +use xcm::{VersionedAsset, VersionedAssetId, VersionedInteriorLocation, VersionedLocation}; + +pub struct PartialBenchmarkInfo { + pub beneficiary: Option, + pub destination: Option, + pub remote_xcm_fee: Option, + pub remote_asset_id: Option, +} + +struct BenchmarkInfo { + beneficiary: VersionedInteriorLocation, + destination: VersionedLocation, + remote_xcm_fee: VersionedAsset, + remote_asset_id: VersionedAssetId, +} + +/// Helper trait implementable by the runtime to set some additional state +/// before the pallet benchmarks are run. +/// +/// This is highly dependent on the runtime logic. +/// If no special conditions are to be met, it can simply be a no-op and return +/// `None`. +pub trait BenchmarkHelper { + fn setup() -> Option; +} + +impl BenchmarkHelper for () { + fn setup() -> Option { + None + } +} + +#[instance_benchmarks(where LocalCurrencyBalanceOf: Into)] +mod benchmarks { + use frame_support::traits::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, + EnsureOrigin, + }; + use frame_system::RawOrigin; + use sp_runtime::traits::{TryConvert, Zero}; + use sp_std::boxed::Box; + use xcm::{ + v4::{Asset, AssetId, Fungibility, Junction, Junctions, Location}, + VersionedAsset, VersionedAssetId, VersionedInteriorLocation, VersionedLocation, + }; + use xcm_executor::traits::TransactAsset; + + use crate::{ + benchmarking::{BenchmarkHelper, BenchmarkInfo, PartialBenchmarkInfo}, + Call, Config, LocalCurrencyBalanceOf, Pallet, SwitchPairStatus, + }; + + fn default_info() -> BenchmarkInfo { + let default_reserve_location: Location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let default_remote_asset_id: AssetId = AssetId(default_reserve_location.clone()); + let default_remote_xcm_fee: Asset = Asset { + id: default_remote_asset_id.clone(), + fun: Fungibility::Fungible(100_000), + }; + let default_beneficiary: Junctions = Junctions::X1( + [Junction::AccountId32 { + network: None, + id: [0; 32], + }] + .into(), + ); + + BenchmarkInfo { + beneficiary: VersionedInteriorLocation::V4(default_beneficiary), + destination: VersionedLocation::V4(default_reserve_location), + remote_asset_id: VersionedAssetId::V4(default_remote_asset_id), + remote_xcm_fee: VersionedAsset::V4(default_remote_xcm_fee), + } + } + + // Return the default info if the helper trait returns `None` or fills any + // `None` field with a default value. + fn fill_with_defaults(benchmark_info: Option) -> BenchmarkInfo { + let default = default_info(); + + let Some(benchmark_info) = benchmark_info else { + return default; + }; + + BenchmarkInfo { + beneficiary: benchmark_info.beneficiary.unwrap_or(default.beneficiary), + destination: benchmark_info.destination.unwrap_or(default.destination), + remote_asset_id: benchmark_info.remote_asset_id.unwrap_or(default.remote_asset_id), + remote_xcm_fee: benchmark_info.remote_xcm_fee.unwrap_or(default.remote_xcm_fee), + } + } + + /// Write a switch pair into storage using the benchmark constants and the + /// `remote_xcm_fee` asset as returned by the benchmark helper, or the + /// default one otherwise. + fn configure_switch_pair() -> BenchmarkInfo + where + T: Config, + I: 'static, + LocalCurrencyBalanceOf: Into, + { + let benchmark_info = >::BenchmarkHelper::setup(); + let BenchmarkInfo { + beneficiary, + destination, + remote_asset_id, + remote_xcm_fee, + } = fill_with_defaults(benchmark_info); + + Pallet::::force_set_switch_pair( + T::RuntimeOrigin::from(RawOrigin::Root), + u128::MAX, + Box::new(remote_asset_id.clone()), + u128::zero(), + Box::new(destination.clone()), + u128::zero(), + Box::new(remote_xcm_fee.clone()), + ) + .unwrap(); + assert!(Pallet::::switch_pair().is_some()); + + BenchmarkInfo { + beneficiary, + destination, + remote_asset_id, + remote_xcm_fee, + } + } + + #[benchmark] + fn set_switch_pair() { + let origin = >::SwitchOrigin::try_successful_origin().unwrap(); + let (reserve_location, remote_asset_id, remote_xcm_fee) = { + let BenchmarkInfo { + destination, + remote_asset_id, + remote_xcm_fee, + .. + } = fill_with_defaults(>::BenchmarkHelper::setup()); + ( + Box::new(destination), + Box::new(remote_asset_id), + Box::new(remote_xcm_fee), + ) + }; + let pool_account = Pallet::::pool_account_id_for_remote_asset(&remote_asset_id).unwrap(); + let local_currency_ed = >::LocalCurrency::minimum_balance(); + >::LocalCurrency::set_balance(&pool_account, local_currency_ed); + + #[extrinsic_call] + Pallet::::set_switch_pair( + origin as T::RuntimeOrigin, + u128::MAX, + remote_asset_id, + u128::zero(), + reserve_location, + u128::zero(), + remote_xcm_fee, + ); + + assert!(Pallet::::switch_pair().is_some()); + } + + #[benchmark] + fn force_set_switch_pair() { + let origin: T::RuntimeOrigin = RawOrigin::Root.into(); + let (reserve_location, remote_asset_id, remote_xcm_fee) = { + let BenchmarkInfo { + destination, + remote_asset_id, + remote_xcm_fee, + .. + } = fill_with_defaults(>::BenchmarkHelper::setup()); + ( + Box::new(destination), + Box::new(remote_asset_id), + Box::new(remote_xcm_fee), + ) + }; + + #[extrinsic_call] + Pallet::::force_set_switch_pair( + origin as T::RuntimeOrigin, + u128::MAX, + remote_asset_id, + u128::zero(), + reserve_location, + u128::zero(), + remote_xcm_fee, + ); + + assert!(Pallet::::switch_pair().is_some()); + } + + #[benchmark] + fn force_unset_switch_pair() { + let origin: T::RuntimeOrigin = RawOrigin::Root.into(); + configure_switch_pair::(); + + #[extrinsic_call] + Pallet::::force_unset_switch_pair(origin as T::RuntimeOrigin); + + assert!(Pallet::::switch_pair().is_none()); + } + + #[benchmark] + fn pause_switch_pair() { + let origin = >::PauseOrigin::try_successful_origin().unwrap(); + configure_switch_pair::(); + + #[extrinsic_call] + Pallet::::pause_switch_pair(origin as T::RuntimeOrigin); + + assert_eq!(Pallet::::switch_pair().unwrap().status, SwitchPairStatus::Paused); + } + + #[benchmark] + fn resume_switch_pair() { + let origin = >::SwitchOrigin::try_successful_origin().unwrap(); + configure_switch_pair::(); + + #[extrinsic_call] + Pallet::::resume_switch_pair(origin as T::RuntimeOrigin); + + assert_eq!(Pallet::::switch_pair().unwrap().status, SwitchPairStatus::Running); + } + + #[benchmark] + fn update_remote_xcm_fee() { + let origin = >::FeeOrigin::try_successful_origin().unwrap(); + let BenchmarkInfo { remote_xcm_fee, .. } = configure_switch_pair::(); + let remote_xcm_fee = Box::new(remote_xcm_fee); + let remote_xcm_fee_2 = remote_xcm_fee.clone(); + + #[extrinsic_call] + Pallet::::update_remote_xcm_fee(origin as T::RuntimeOrigin, remote_xcm_fee); + + assert_eq!(Pallet::::switch_pair().unwrap().remote_xcm_fee, *remote_xcm_fee_2); + } + + #[benchmark] + fn switch() { + let origin = >::SubmitterOrigin::try_successful_origin().unwrap(); + let BenchmarkInfo { + beneficiary, + destination, + remote_xcm_fee, + remote_asset_id, + } = configure_switch_pair::(); + Pallet::::resume_switch_pair(>::SwitchOrigin::try_successful_origin().unwrap()).unwrap(); + let account_id = >::SubmitterOrigin::ensure_origin(origin.clone()).unwrap(); + let pool_account = Pallet::::pool_account_id_for_remote_asset(&remote_asset_id).unwrap(); + let minimum_balance = >::LocalCurrency::minimum_balance(); + // Set submitter balance to ED + 1_000 and pool balance to ED + { + >::LocalCurrency::set_balance(&account_id, minimum_balance + 1_000u32.into()); + >::LocalCurrency::set_balance(&pool_account, minimum_balance); + } + // Set submitter's fungible balance to the XCM fee + let local_account_id_junction = >::AccountIdConverter::try_convert(account_id).unwrap(); + { + >::AssetTransactor::deposit_asset( + &remote_xcm_fee.try_into().unwrap(), + &(local_account_id_junction.into()), + None, + ) + .unwrap(); + } + + // Push the beneficiary to the returned `destination` value. + let beneficiary = Box::new( + Location::try_from(destination) + .unwrap() + .appended_with(Junctions::try_from(beneficiary).unwrap()) + .unwrap() + .into(), + ); + let amount = 1_000u32.into(); + + #[extrinsic_call] + Pallet::::switch(origin as T::RuntimeOrigin, amount, beneficiary); + + assert_eq!( + >::LocalCurrency::balance(&pool_account), + minimum_balance + 1_000u32.into() + ); + } + + #[cfg(test)] + mod benchmark_tests { + use crate::Pallet; + + frame_benchmarking::impl_benchmark_test_suite!( + Pallet, + crate::mock::ExtBuilder::default().build_with_keystore(), + crate::mock::MockRuntime + ); + } +} diff --git a/pallets/pallet-asset-switch/src/default_weights.rs b/pallets/pallet-asset-switch/src/default_weights.rs new file mode 100644 index 000000000..25d3b03f3 --- /dev/null +++ b/pallets/pallet-asset-switch/src/default_weights.rs @@ -0,0 +1,252 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +//! Autogenerated weights for pallet_asset_switch +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 27.0.0 +//! DATE: 2024-07-17 +//! STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `rust-2`, CPU: `12th Gen Intel(R) Core(TM) i9-12900K` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/debug/kilt-parachain +// benchmark +// pallet +// --template=.maintain/weight-template.hbs +// --header=HEADER-GPL +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --steps=50 +// --repeat=20 +// --chain +// dev +// --pallet +// pallet-asset-switch +// --extrinsic=* +// --output +// ./pallets/pallet-asset-switch/src/default_weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_asset_switch. +pub trait WeightInfo { + fn set_switch_pair() -> Weight; + fn force_set_switch_pair() -> Weight; + fn force_unset_switch_pair() -> Weight; + fn pause_switch_pair() -> Weight; + fn resume_switch_pair() -> Weight; + fn update_remote_xcm_fee() -> Weight; + fn switch() -> Weight; +} + +/// Weights for pallet_asset_switch using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + fn set_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `128` + // Estimated: `3597` + // Minimum execution time: 193_033 nanoseconds. + Weight::from_parts(200_247_000, 3597) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:0 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn force_set_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 125_622 nanoseconds. + Weight::from_parts(126_582_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn force_unset_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `1657` + // Minimum execution time: 145_639 nanoseconds. + Weight::from_parts(150_383_000, 1657) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn pause_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `1657` + // Minimum execution time: 101_363 nanoseconds. + Weight::from_parts(102_543_000, 1657) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn resume_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `1657` + // Minimum execution time: 151_273 nanoseconds. + Weight::from_parts(156_371_000, 1657) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn update_remote_xcm_fee() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `1657` + // Minimum execution time: 104_981 nanoseconds. + Weight::from_parts(105_850_000, 1657) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0) + /// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1) + /// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0) + /// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + fn switch() -> Weight { + // Proof Size summary in bytes: + // Measured: `837` + // Estimated: `6204` + // Minimum execution time: 1_279_064 nanoseconds. + Weight::from_parts(1_289_730_000, 6204) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + fn set_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `128` + // Estimated: `3597` + // Minimum execution time: 193_033 nanoseconds. + Weight::from_parts(200_247_000, 3597) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:0 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn force_set_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 125_622 nanoseconds. + Weight::from_parts(126_582_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn force_unset_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `1657` + // Minimum execution time: 145_639 nanoseconds. + Weight::from_parts(150_383_000, 1657) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn pause_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `1657` + // Minimum execution time: 101_363 nanoseconds. + Weight::from_parts(102_543_000, 1657) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn resume_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `1657` + // Minimum execution time: 151_273 nanoseconds. + Weight::from_parts(156_371_000, 1657) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn update_remote_xcm_fee() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `1657` + // Minimum execution time: 104_981 nanoseconds. + Weight::from_parts(105_850_000, 1657) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0) + /// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1) + /// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0) + /// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + fn switch() -> Weight { + // Proof Size summary in bytes: + // Measured: `837` + // Estimated: `6204` + // Minimum execution time: 1_279_064 nanoseconds. + Weight::from_parts(1_289_730_000, 6204) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } +} diff --git a/pallets/pallet-asset-switch/src/lib.rs b/pallets/pallet-asset-switch/src/lib.rs new file mode 100644 index 000000000..85ee4c791 --- /dev/null +++ b/pallets/pallet-asset-switch/src/lib.rs @@ -0,0 +1,684 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +#![cfg_attr(not(feature = "std"), no_std)] +#![doc = include_str!("../README.md")] + +pub mod traits; +pub mod xcm; + +mod default_weights; +pub use default_weights::WeightInfo; +mod switch; +pub use switch::{SwitchPairInfo, SwitchPairStatus}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +#[cfg(any(feature = "try-runtime", test))] +mod try_state; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +#[cfg(feature = "runtime-benchmarks")] +pub use benchmarking::{BenchmarkHelper, PartialBenchmarkInfo}; + +use ::xcm::{VersionedAsset, VersionedAssetId, VersionedLocation}; +use frame_support::traits::{ + fungible::Inspect, + tokens::{Fortitude, Preservation}, + PalletInfoAccess, +}; +use parity_scale_codec::{Decode, Encode}; +use sp_runtime::traits::TrailingZeroInput; +use sp_std::boxed::Box; + +pub use crate::pallet::*; + +const LOG_TARGET: &str = "runtime::pallet-asset-switch"; + +#[frame_support::pallet] +pub mod pallet { + use crate::{ + switch::{NewSwitchPairInfo, SwitchPairInfo, SwitchPairStatus}, + traits::SwitchHooks, + WeightInfo, LOG_TARGET, + }; + + use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, + tokens::{Preservation, Provenance}, + EnsureOrigin, + }, + }; + use frame_system::{ensure_root, pallet_prelude::*}; + use sp_runtime::traits::{TryConvert, Zero}; + use sp_std::{boxed::Box, vec}; + use xcm::{ + v4::{ + validate_send, Asset, AssetFilter, AssetId, + Instruction::{BuyExecution, DepositAsset, RefundSurplus, SetAppendix, TransferAsset, WithdrawAsset}, + Junction, Location, SendXcm, WeightLimit, WildAsset, Xcm, + }, + VersionedAsset, VersionedAssetId, VersionedLocation, + }; + use xcm_executor::traits::TransactAsset; + + pub type LocalCurrencyBalanceOf = + <>::LocalCurrency as InspectFungible<::AccountId>>::Balance; + pub type SwitchPairInfoOf = SwitchPairInfo<::AccountId>; + pub type NewSwitchPairInfoOf = NewSwitchPairInfo<::AccountId>; + + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// How to convert a local `AccountId` to a `Junction`, for the purpose + /// of taking XCM fees from the user's balance via the configured + /// `AssetTransactor`. + type AccountIdConverter: TryConvert; + /// The asset transactor to charge user's for XCM fees as specified in + /// the switch pair. + type AssetTransactor: TransactAsset; + /// The origin that can update the XCM fee for a switch pair. + type FeeOrigin: EnsureOrigin; + /// The local currency. + type LocalCurrency: MutateFungible; + /// The origin that can pause switches in both directions. + type PauseOrigin: EnsureOrigin; + /// The aggregate event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The origin that can request a switch of some local tokens for some + /// remote assets. + type SubmitterOrigin: EnsureOrigin; + /// Runtime-injected logic to execute before and after a local -> remote + /// and remote -> local switch. + type SwitchHooks: SwitchHooks; + /// The origin that can set a new switch pair, remove one, or resume + /// switches. + type SwitchOrigin: EnsureOrigin; + type WeightInfo: WeightInfo; + /// The XCM router to route XCM transfers to the configured reserve + /// location. + type XcmRouter: SendXcm; + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: crate::benchmarking::BenchmarkHelper; + } + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet + where + LocalCurrencyBalanceOf: Into, + { + #[cfg(feature = "try-runtime")] + fn try_state(n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + crate::try_state::do_try_state::(n) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A new switch pair is created. + SwitchPairCreated { + remote_asset_circulating_supply: u128, + remote_asset_ed: u128, + pool_account: T::AccountId, + remote_asset_id: VersionedAssetId, + remote_reserve_location: VersionedLocation, + remote_xcm_fee: Box, + remote_asset_total_supply: u128, + }, + /// A switch pair is removed. + SwitchPairRemoved { remote_asset_id: VersionedAssetId }, + /// A switch pair has enabled switches. + SwitchPairResumed { remote_asset_id: VersionedAssetId }, + /// A switch pair has suspended switches. + SwitchPairPaused { remote_asset_id: VersionedAssetId }, + /// The XCM fee for the switch has been updated. + SwitchPairFeeUpdated { old: VersionedAsset, new: VersionedAsset }, + /// A switch of local -> remote asset has taken place. + LocalToRemoteSwitchExecuted { + from: T::AccountId, + to: VersionedLocation, + amount: LocalCurrencyBalanceOf, + }, + /// A switch of remote -> local asset has taken place. + RemoteToLocalSwitchExecuted { to: T::AccountId, amount: u128 }, + } + + #[pallet::error] + pub enum Error { + /// Provided switch pair info is not valid. + InvalidInput, + /// The runtime-injected logic returned an error with a specific code. + Hook(u8), + /// There are not enough remote assets to cover the specified amount of + /// local tokens to switch. + Liquidity, + /// Failure in transferring the local tokens from the user's balance to + /// the switch pair pool account. + LocalPoolBalance, + /// The calculated switch pair pool account does not have enough local + /// tokens to cover the specified `circulating_supply`. + PoolInitialLiquidityRequirement, + /// A switch pair has already been set. + SwitchPairAlreadyExisting, + /// The switch pair did not enable switches. + SwitchPairNotEnabled, + /// No switch pair found. + SwitchPairNotFound, + /// The user does not have enough local tokens to cover the requested + /// switch. + UserSwitchBalance, + /// The user does not have enough assets to pay for the remote XCM fees. + UserXcmBalance, + /// Something regarding XCM went wrong. + Xcm, + /// Internal error. + Internal, + } + + #[pallet::storage] + #[pallet::getter(fn switch_pair)] + pub(crate) type SwitchPair, I: 'static = ()> = StorageValue<_, SwitchPairInfoOf, OptionQuery>; + + #[pallet::call] + impl, I: 'static> Pallet + where + LocalCurrencyBalanceOf: Into, + { + /// Set a new switch pair. + /// + /// See the crate's README for more. + #[pallet::call_index(0)] + #[pallet::weight(>::WeightInfo::set_switch_pair())] + pub fn set_switch_pair( + origin: OriginFor, + remote_asset_total_supply: u128, + remote_asset_id: Box, + remote_asset_circulating_supply: u128, + remote_reserve_location: Box, + remote_asset_ed: u128, + remote_xcm_fee: Box, + ) -> DispatchResult { + T::SwitchOrigin::ensure_origin(origin)?; + + // 1. Verify switch pair has not already been set. + ensure!(!SwitchPair::::exists(), Error::::SwitchPairAlreadyExisting); + + // 2. Verify that total issuance >= circulating supply and that the amount of + // remote assets locked (total - circulating) is greater than the minimum + // amount required at destination (remote ED). + ensure!( + remote_asset_total_supply >= remote_asset_circulating_supply.saturating_add(remote_asset_ed), + Error::::InvalidInput + ); + + // 3. Verify the pool account has enough local assets to match the circulating + // supply of eKILTs to cover for all potential remote -> local switches. + // Handle the special case where circulating supply is `0`. + let pool_account = Self::pool_account_id_for_remote_asset(&remote_asset_id)?; + let pool_account_reducible_balance_as_u128: u128 = Self::get_pool_reducible_balance(&pool_account).into(); + let pool_account_total_balance_as_u128: u128 = T::LocalCurrency::balance(&pool_account).into(); + if pool_account_total_balance_as_u128.is_zero() + || pool_account_reducible_balance_as_u128 < remote_asset_circulating_supply + { + // If the pool account has `0` available tokens, or if it has some tokens, but + // not enough to cover the specified circulating supply, fail. + Err(Error::::PoolInitialLiquidityRequirement) + } else { + // Otherwise, we can accept the current parameters. + Ok(()) + }?; + + Self::set_switch_pair_bypass_checks( + remote_asset_total_supply, + *remote_asset_id, + remote_asset_circulating_supply, + *remote_reserve_location, + remote_asset_ed, + *remote_xcm_fee, + pool_account, + ); + + Ok(()) + } + + /// Force-set a new switch pair. + /// + /// See the crate's README for more. + #[pallet::call_index(1)] + #[pallet::weight(>::WeightInfo::force_set_switch_pair())] + pub fn force_set_switch_pair( + origin: OriginFor, + remote_asset_total_supply: u128, + remote_asset_id: Box, + remote_asset_circulating_supply: u128, + remote_reserve_location: Box, + remote_asset_ed: u128, + remote_xcm_fee: Box, + ) -> DispatchResult { + ensure_root(origin)?; + + ensure!( + remote_asset_total_supply >= remote_asset_circulating_supply.saturating_add(remote_asset_ed), + Error::::InvalidInput + ); + let pool_account = Self::pool_account_id_for_remote_asset(&remote_asset_id)?; + + Self::set_switch_pair_bypass_checks( + remote_asset_total_supply, + *remote_asset_id, + remote_asset_circulating_supply, + *remote_reserve_location, + remote_asset_ed, + *remote_xcm_fee, + pool_account, + ); + + Ok(()) + } + + /// Unset a switch pair. + /// + /// See the crate's README for more. + #[pallet::call_index(2)] + #[pallet::weight(>::WeightInfo::force_unset_switch_pair())] + pub fn force_unset_switch_pair(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + + Self::unset_switch_pair_bypass_checks(); + + Ok(()) + } + + /// Pause switches for a switch pair. + /// + /// See the crate's README for more. + #[pallet::call_index(3)] + #[pallet::weight(>::WeightInfo::pause_switch_pair())] + pub fn pause_switch_pair(origin: OriginFor) -> DispatchResult { + T::PauseOrigin::ensure_origin(origin)?; + + Self::set_switch_pair_status(SwitchPairStatus::Paused)?; + + Ok(()) + } + + /// Resume switches for a switch pair. + /// + /// See the crate's README for more. + #[pallet::call_index(4)] + #[pallet::weight(>::WeightInfo::resume_switch_pair())] + pub fn resume_switch_pair(origin: OriginFor) -> DispatchResult { + T::SwitchOrigin::ensure_origin(origin)?; + + Self::set_switch_pair_status(SwitchPairStatus::Running)?; + + Ok(()) + } + + /// Update the remote XCM fee for a switch pair. + /// + /// See the crate's README for more. + #[pallet::call_index(5)] + #[pallet::weight(>::WeightInfo::update_remote_xcm_fee())] + pub fn update_remote_xcm_fee(origin: OriginFor, new: Box) -> DispatchResult { + T::FeeOrigin::ensure_origin(origin)?; + + SwitchPair::::try_mutate(|entry| { + let SwitchPairInfoOf:: { remote_xcm_fee, .. } = + entry.as_mut().ok_or(Error::::SwitchPairNotFound)?; + let old_remote_xcm_fee = remote_xcm_fee.clone(); + *remote_xcm_fee = *new.clone(); + if old_remote_xcm_fee != *new { + Self::deposit_event(Event::::SwitchPairFeeUpdated { + old: old_remote_xcm_fee, + new: *new, + }); + }; + Ok::<_, Error>(()) + })?; + + Ok(()) + } + + /// Perform a local -> remote asset switch. + /// + /// See the crate's README for more. + #[pallet::call_index(6)] + #[pallet::weight(>::WeightInfo::switch())] + pub fn switch( + origin: OriginFor, + local_asset_amount: LocalCurrencyBalanceOf, + beneficiary: Box, + ) -> DispatchResult { + let submitter = T::SubmitterOrigin::ensure_origin(origin)?; + + // 1. Retrieve switch pair info from storage, else fail. + let switch_pair = + SwitchPair::::get().ok_or(DispatchError::from(Error::::SwitchPairNotFound))?; + + // 2. Check if switches are enabled. + ensure!( + switch_pair.is_enabled(), + DispatchError::from(Error::::SwitchPairNotEnabled) + ); + + // 3. Verify the tx submitter has enough local assets for the switch, without + // having their balance go to zero. + T::LocalCurrency::can_withdraw(&submitter, local_asset_amount) + .into_result(true) + .map_err(|e| { + log::info!("Failed to withdraw balance from submitter with error {:?}", e); + DispatchError::from(Error::::UserSwitchBalance) + })?; + + // 4. Verify the local assets can be transferred to the switch pool account. + // This could fail if the pool's balance is `0` and the sent amount is less + // than ED. + T::LocalCurrency::can_deposit(&switch_pair.pool_account, local_asset_amount, Provenance::Extant) + .into_result() + .map_err(|e| { + log::info!("Failed to deposit amount into pool account with error {:?}", e); + DispatchError::from(Error::::LocalPoolBalance) + })?; + + // 5. Verify we have enough balance (minus ED, already substracted from the + // stored balance info) on the remote location to perform the transfer. + let remote_asset_amount_as_u128 = local_asset_amount.into(); + ensure!( + switch_pair.reducible_remote_balance() >= remote_asset_amount_as_u128, + Error::::Liquidity + ); + + let asset_id_v4: AssetId = switch_pair.remote_asset_id.clone().try_into().map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert asset ID {:?} into v4 `AssetId` with error {:?}", + switch_pair.remote_asset_id, + e + ); + DispatchError::from(Error::::Internal) + })?; + let remote_asset_fee_v4: Asset = switch_pair.remote_xcm_fee.clone().try_into().map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert remote XCM asset fee {:?} into v4 `Asset` with error {:?}", + switch_pair.remote_xcm_fee, + e + ); + DispatchError::from(Error::::Xcm) + })?; + let destination_v4: Location = switch_pair.remote_reserve_location.clone().try_into().map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert remote reserve location {:?} into v4 `Location` with error {:?}", + switch_pair.remote_reserve_location, + e + ); + DispatchError::from(Error::::Internal) + })?; + let beneficiary_v4: Location = (*beneficiary.clone()).try_into().map_err(|e| { + log::info!( + target: LOG_TARGET, + "Failed to convert beneficiary {:?} into v4 `Location` with error {:?}", + beneficiary, + e + ); + DispatchError::from(Error::::Xcm) + })?; + // Use the same local `AccountIdConverter` to generate a `Location` to use + // to send funds on remote. + let submitter_as_location = T::AccountIdConverter::try_convert(submitter.clone()) + .map(|j| j.into_location()) + .map_err(|e| { + log::info!( + target: LOG_TARGET, + "Failed to convert account {:?} into `Location` with error {:?}", + submitter, + e + ); + DispatchError::from(Error::::Xcm) + })?; + + // 6. Compose and validate XCM message + let appendix: Xcm<()> = vec![ + RefundSurplus, + DepositAsset { + assets: AssetFilter::Wild(WildAsset::All), + beneficiary: submitter_as_location.clone(), + }, + ] + .into(); + let remote_xcm: Xcm<()> = vec![ + WithdrawAsset(remote_asset_fee_v4.clone().into()), + BuyExecution { + weight_limit: WeightLimit::Unlimited, + fees: remote_asset_fee_v4.clone(), + }, + TransferAsset { + assets: (asset_id_v4, remote_asset_amount_as_u128).into(), + beneficiary: beneficiary_v4, + }, + SetAppendix(appendix), + ] + .into(); + let xcm_ticket = + validate_send::(destination_v4.clone(), remote_xcm.clone()).map_err(|e| { + log::info!( + "Failed to call validate_send for destination {:?} and remote XCM {:?} with error {:?}", + destination_v4, + remote_xcm, + e + ); + DispatchError::from(Error::::Xcm) + })?; + + // 7. Call into hook pre-switch checks + T::SwitchHooks::pre_local_to_remote_switch(&submitter, &beneficiary, local_asset_amount) + .map_err(|e| DispatchError::from(Error::::Hook(e.into())))?; + + // 8. Transfer funds from user to pool + let transferred_amount = T::LocalCurrency::transfer( + &submitter, + &switch_pair.pool_account, + local_asset_amount, + // We don't care if the submitter's account gets dusted, but it should not be killed. + Preservation::Protect, + )?; + if transferred_amount != local_asset_amount { + log::error!( + "Transferred amount {:?} does not match expected user-specified amount {:?}", + transferred_amount, + local_asset_amount + ); + return Err(Error::::Internal.into()); + } + + // 9. Take XCM fee from submitter. + let withdrawn_fees = T::AssetTransactor::withdraw_asset(&remote_asset_fee_v4, &submitter_as_location, None) + .map_err(|e| { + log::info!( + target: LOG_TARGET, + "Failed to withdraw asset {:?} from location {:?} with error {:?}", + remote_asset_fee_v4, + submitter_as_location, + e + ); + DispatchError::from(Error::::UserXcmBalance) + })?; + if withdrawn_fees != vec![remote_asset_fee_v4.clone()].into() { + log::error!( + target: LOG_TARGET, + "Withdrawn fees {:?} does not match expected fee {:?}.", + withdrawn_fees, + remote_asset_fee_v4 + ); + return Err(DispatchError::from(Error::::Internal)); + } + + // 10. Send XCM out + T::XcmRouter::deliver(xcm_ticket.0).map_err(|e| { + log::info!("Failed to deliver ticket with error {:?}", e); + DispatchError::from(Error::::Xcm) + })?; + + // 11. Update remote asset balance and circulating supply. + SwitchPair::::try_mutate(|entry| { + let Some(switch_pair_info) = entry.as_mut() else { + log::error!(target: LOG_TARGET, "Failed to borrow stored switch pair info as mut."); + return Err(Error::::Internal); + }; + switch_pair_info + .try_process_outgoing_switch(remote_asset_amount_as_u128) + .map_err(|_| { + log::error!( + target: LOG_TARGET, + "Failed to account for local to remote switch of {:?} tokens.", + remote_asset_amount_as_u128 + ); + Error::::Internal + })?; + Ok(()) + })?; + + // 12. Call into hook post-switch checks + T::SwitchHooks::post_local_to_remote_switch(&submitter, &beneficiary, local_asset_amount) + .map_err(|e| DispatchError::from(Error::::Hook(e.into())))?; + + Self::deposit_event(Event::::LocalToRemoteSwitchExecuted { + from: submitter, + to: *beneficiary, + amount: local_asset_amount, + }); + + Ok(()) + } + } +} + +impl, I: 'static> Pallet { + pub(crate) fn get_pool_reducible_balance(pool_address: &T::AccountId) -> LocalCurrencyBalanceOf { + T::LocalCurrency::reducible_balance(pool_address, Preservation::Preserve, Fortitude::Polite) + } +} + +impl, I: 'static> Pallet { + fn set_switch_pair_bypass_checks( + remote_asset_total_supply: u128, + remote_asset_id: VersionedAssetId, + remote_asset_circulating_supply: u128, + remote_reserve_location: VersionedLocation, + remote_asset_ed: u128, + remote_xcm_fee: VersionedAsset, + pool_account: T::AccountId, + ) { + debug_assert!( + remote_asset_total_supply >= remote_asset_circulating_supply.saturating_add(remote_asset_ed), + "Provided total issuance smaller than circulating supply + remote asset ED." + ); + + let switch_pair_info = SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account.clone(), + remote_asset_circulating_supply, + remote_asset_ed, + remote_asset_id: remote_asset_id.clone(), + remote_asset_total_supply, + remote_xcm_fee: remote_xcm_fee.clone(), + remote_reserve_location: remote_reserve_location.clone(), + status: Default::default(), + }); + + SwitchPair::::set(Some(switch_pair_info)); + + Self::deposit_event(Event::::SwitchPairCreated { + remote_asset_circulating_supply, + remote_asset_ed, + pool_account, + remote_reserve_location, + remote_asset_id, + remote_xcm_fee: Box::new(remote_xcm_fee), + remote_asset_total_supply, + }); + } + + fn unset_switch_pair_bypass_checks() { + let switch_pair = SwitchPair::::take(); + if let Some(switch_pair) = switch_pair { + Self::deposit_event(Event::::SwitchPairRemoved { + remote_asset_id: switch_pair.remote_asset_id, + }); + }; + } + + fn set_switch_pair_status(new_status: SwitchPairStatus) -> Result<(), Error> { + SwitchPair::::try_mutate(|entry| { + let SwitchPairInfoOf:: { + remote_asset_id, + status, + .. + } = entry.as_mut().ok_or(Error::::SwitchPairNotFound)?; + let relevant_event = match new_status { + SwitchPairStatus::Running => Event::::SwitchPairResumed { + remote_asset_id: remote_asset_id.clone(), + }, + SwitchPairStatus::Paused => Event::::SwitchPairPaused { + remote_asset_id: remote_asset_id.clone(), + }, + }; + let old_status = status.clone(); + *status = new_status; + // If state was actually changed, generate an event, otherwise this is a no-op. + if old_status != *status { + Self::deposit_event(relevant_event); + } + Ok::<_, Error>(()) + })?; + Ok(()) + } +} + +impl, I: 'static> Pallet { + /// Derive an `AccountId` for the provided `remote_asset_id` and the + /// pallet's name as configured in the runtime. + pub fn pool_account_id_for_remote_asset(remote_asset_id: &VersionedAssetId) -> Result> { + let pallet_name = as PalletInfoAccess>::name(); + let pallet_name_hashed = sp_io::hashing::blake2_256(pallet_name.as_bytes()); + let hash_input = (pallet_name_hashed, b'.', remote_asset_id.clone()).encode(); + let hash_output = sp_io::hashing::blake2_256(hash_input.as_slice()); + T::AccountId::decode(&mut TrailingZeroInput::new(hash_output.as_slice())).map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to generate pool ID from remote asset {:?} with error: {:?}", + remote_asset_id, + e + ); + Error::::Internal + }) + } +} diff --git a/pallets/pallet-asset-switch/src/mock.rs b/pallets/pallet-asset-switch/src/mock.rs new file mode 100644 index 000000000..899b471d2 --- /dev/null +++ b/pallets/pallet-asset-switch/src/mock.rs @@ -0,0 +1,321 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + construct_runtime, storage_alias, + traits::{ + fungible::{Mutate, MutateFreeze, MutateHold}, + Everything, VariantCount, + }, + Twox64Concat, +}; +use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_core::{ConstU16, ConstU32, ConstU64, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, +}; +use sp_std::sync::Arc; +use xcm::v4::{ + Asset, AssetId, Error as XcmError, Fungibility, + Junction::{AccountId32 as AccountId32Junction, AccountKey20, GlobalConsensus, Parachain}, + Junctions::{Here, X1, X2}, + Location, NetworkId, SendError, SendResult, SendXcm, Xcm, XcmContext, XcmHash, +}; +use xcm_executor::{traits::TransactAsset, AssetsInHolding}; + +use crate::{xcm::convert::AccountId32ToAccountId32JunctionConverter, Config, NewSwitchPairInfoOf, Pallet}; + +construct_runtime!( + pub enum MockRuntime { + System: frame_system, + Balances: pallet_balances, + Assetswitch: crate + } +); + +impl frame_system::Config for MockRuntime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId32; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = ConstU64<256>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeTask = (); + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = ConstU16<1>; + type SystemWeightInfo = (); + type Version = (); +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, MaxEncodedLen, Encode, Decode, Debug, TypeInfo, Default)] +pub struct MockRuntimeHoldReason; + +impl VariantCount for MockRuntimeHoldReason { + const VARIANT_COUNT: u32 = 1; +} + +impl pallet_balances::Config for MockRuntime { + type AccountStore = System; + type Balance = u64; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type FreezeIdentifier = [u8; 1]; + type MaxFreezes = ConstU32<10>; + type MaxLocks = ConstU32<10>; + type MaxReserves = ConstU32<10>; + type ReserveIdentifier = [u8; 1]; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = MockRuntimeHoldReason; + type RuntimeFreezeReason = (); + type WeightInfo = (); +} + +// Used to temporarily store balances used in the mock. +#[storage_alias] +type BalancesStorage = StorageMap, Twox64Concat, Location, u128>; + +pub struct MockFungibleAssetTransactor; + +impl MockFungibleAssetTransactor { + pub(super) fn get_balance_for(account: &Location) -> u128 { + BalancesStorage::::get(account).unwrap_or_default() + } +} + +impl TransactAsset for MockFungibleAssetTransactor { + fn withdraw_asset( + what: &Asset, + who: &Location, + _maybe_context: Option<&XcmContext>, + ) -> Result { + let Asset { + fun: Fungibility::Fungible(amount), + .. + } = *what + else { + return Err(XcmError::FailedToTransactAsset("Only fungible assets supported.")); + }; + BalancesStorage::::try_mutate(who, |entry| { + let balance = entry + .as_mut() + .ok_or(XcmError::FailedToTransactAsset("No balance found for user."))?; + let new_balance = balance + .checked_sub(amount) + .ok_or(XcmError::FailedToTransactAsset("No enough balance for user."))?; + *balance = new_balance; + Ok::<_, XcmError>(()) + })?; + Ok(vec![what.clone()].into()) + } + + fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> Result<(), XcmError> { + let Asset { + fun: Fungibility::Fungible(amount), + .. + } = *what + else { + return Err(XcmError::FailedToTransactAsset("Only fungible assets supported.")); + }; + BalancesStorage::::mutate(who, |entry| { + let new_balance = entry + .unwrap_or_default() + .checked_add(amount) + .ok_or(XcmError::FailedToTransactAsset("Balance overflow for destination."))?; + *entry = Some(new_balance); + Ok::<_, XcmError>(()) + })?; + Ok(()) + } +} + +pub struct AlwaysSuccessfulXcmRouter; + +impl SendXcm for AlwaysSuccessfulXcmRouter { + type Ticket = (); + + fn validate(_destination: &mut Option, _message: &mut Option>) -> SendResult { + Ok(((), vec![].into())) + } + + fn deliver(_ticket: Self::Ticket) -> Result { + Ok(XcmHash::default()) + } +} + +impl crate::Config for MockRuntime { + type AccountIdConverter = AccountId32ToAccountId32JunctionConverter; + type AssetTransactor = MockFungibleAssetTransactor; + type FeeOrigin = EnsureRoot; + type LocalCurrency = Balances; + type PauseOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SubmitterOrigin = EnsureSigned; + type SwitchHooks = (); + type SwitchOrigin = EnsureRoot; + type WeightInfo = (); + type XcmRouter = AlwaysSuccessfulXcmRouter; + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +#[derive(Default)] +pub(super) struct ExtBuilder( + Option>, + Vec<(AccountId32, u64, u64, u64)>, + Vec<(AccountId32, Asset)>, +); + +pub(super) const FREEZE_REASON: [u8; 1] = *b"1"; +pub(super) const HOLD_REASON: MockRuntimeHoldReason = MockRuntimeHoldReason {}; + +impl ExtBuilder { + pub(super) fn with_switch_pair_info(mut self, switch_pair_info: NewSwitchPairInfoOf) -> Self { + self.0 = Some(switch_pair_info); + self + } + + pub(super) fn with_balances(mut self, balances: Vec<(AccountId32, u64, u64, u64)>) -> Self { + self.1 = balances; + self + } + + pub(super) fn with_fungibles(mut self, fungibles: Vec<(AccountId32, Asset)>) -> Self { + self.2 = fungibles; + self + } + + pub(super) fn build(self) -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + System::set_block_number(1); + + if let Some(switch_pair_info) = self.0 { + Pallet::::set_switch_pair_bypass_checks( + switch_pair_info.remote_asset_total_supply, + switch_pair_info.remote_asset_id, + switch_pair_info.remote_asset_circulating_supply, + switch_pair_info.remote_reserve_location, + switch_pair_info.remote_asset_ed, + switch_pair_info.remote_xcm_fee, + switch_pair_info.pool_account, + ); + Pallet::::set_switch_pair_status(switch_pair_info.status).unwrap(); + } + for (account, free, frozen, held) in self.1 { + >::set_balance(&account, free); + >::set_freeze(&FREEZE_REASON, &account, frozen) + .expect("Failed to freeze balance on account."); + >::hold(&HOLD_REASON, &account, held) + .expect("Failed to hold balance on account."); + } + + for (account, asset) in self.2 { + MockFungibleAssetTransactor::deposit_asset( + &asset, + &Location { + parents: 0, + interior: X1([AccountId32Junction { + network: None, + id: account.clone().into(), + }] + .into()), + }, + Some(&XcmContext::with_message_id([0; 32])), + ) + .unwrap_or_else(|_| { + panic!( + "Should not fail to deposit asset {:?} into account {:?}", + asset, account + ) + }); + } + + // Some setup operations generate events which interfere with our assertions. + System::reset_events() + }); + + ext + } + + // Run the specified closure and test the storage invariants afterwards. + pub(crate) fn build_and_execute_with_sanity_tests(self, run: impl FnOnce()) { + let mut ext = self.build(); + ext.execute_with(|| { + run(); + crate::try_state::do_try_state::(System::block_number()).unwrap(); + }); + } + + #[cfg(all(feature = "runtime-benchmarks", test))] + pub(crate) fn build_with_keystore(self) -> sp_io::TestExternalities { + let mut ext = self.build(); + let keystore = sp_keystore::testing::MemoryKeystore::new(); + ext.register_extension(sp_keystore::KeystoreExt(sp_std::sync::Arc::new(keystore))); + ext + } +} + +pub(super) const XCM_ASSET_FEE: Asset = Asset { + id: PARENT_NATIVE_CURRENCY, + fun: Fungibility::Fungible(1_000), +}; +const PARENT_NATIVE_CURRENCY: AssetId = AssetId(PARENT_LOCATION); +const PARENT_LOCATION: Location = Location { + parents: 1, + interior: Here, +}; + +pub(super) fn get_asset_hub_location() -> Location { + Location { + parents: 1, + interior: X1(Arc::new([Parachain(1_000)])), + } +} + +pub(super) fn get_remote_erc20_asset_id() -> AssetId { + AssetId(Location { + parents: 2, + interior: X2([ + GlobalConsensus(NetworkId::Ethereum { chain_id: 1 }), + AccountKey20 { + network: None, + key: *b"!!test_eth_address!!", + }, + ] + .into()), + }) +} diff --git a/pallets/pallet-asset-switch/src/switch.rs b/pallets/pallet-asset-switch/src/switch.rs new file mode 100644 index 000000000..bebb77f33 --- /dev/null +++ b/pallets/pallet-asset-switch/src/switch.rs @@ -0,0 +1,168 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_core::RuntimeDebug; +use xcm::{VersionedAsset, VersionedAssetId, VersionedLocation}; + +/// Input information used to generate a `SwitchPairInfo`. +#[derive(Encode, Decode, TypeInfo, RuntimeDebug, Clone)] +pub struct NewSwitchPairInfo { + /// The address that will hold the local tokens locked in return for the + /// remote asset. + pub pool_account: AccountId, + /// The circulating supply, i.e., the total supply - required EDs for both + /// local and remote assets - supply controlled by the chain on the remote + /// reserve location. + pub remote_asset_circulating_supply: u128, + /// The existential deposit (i.e., minimum balance to hold) of the remote + /// asset. + pub remote_asset_ed: u128, + /// The ID of the remote asset to switch 1:1 with the local token. + pub remote_asset_id: VersionedAssetId, + /// The total supply of the remote asset. This is assumed to never change. + /// If it does, the current pool must be manually updated to reflect the + /// changes. + pub remote_asset_total_supply: u128, + /// The remote location on which the remote asset lives. + pub remote_reserve_location: VersionedLocation, + /// The assets to take from the user's balance on this chain to pay for XCM + /// execution fees on the reserve location. + pub remote_xcm_fee: VersionedAsset, + /// The status of the switch pair. + pub status: SwitchPairStatus, +} + +/// Information related to a switch pair. +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Eq, RuntimeDebug, Clone)] +pub struct SwitchPairInfo { + /// The address that will hold the local tokens locked in return for the + /// remote asset. + pub pool_account: AccountId, + /// The circulating supply, i.e., the total supply - required EDs for both + /// local and remote assets - supply controlled by the chain on the remote + /// reserve location. + pub remote_asset_circulating_supply: u128, + /// The existential deposit (i.e., minimum balance to hold) of the remote + /// asset. + pub remote_asset_ed: u128, + /// The ID of the remote asset to switch 1:1 with the local token. + pub remote_asset_id: VersionedAssetId, + /// The total supply of the remote asset. This is assumed to never change. + /// If it does, the current pool must be manually updated to reflect the + /// changes. + pub remote_asset_total_supply: u128, + /// The remote location on which the remote asset lives. + pub remote_reserve_location: VersionedLocation, + /// The assets to take from the user's balance on this chain to pay for XCM + /// execution fees on the reserve location. + pub remote_xcm_fee: VersionedAsset, + /// The status of the switch pair. + pub status: SwitchPairStatus, + + /// The balance of the remote (fungible) asset for the chain sovereign + /// account on the configured `remote_reserve_location`. This includes the + /// ED for the remote asset, as specified by the `remote_asset_ed` property. + remote_asset_sovereign_total_balance: u128, +} + +/// All statues a switch pool can be in at any given time. +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Eq, RuntimeDebug, Clone, Default)] +pub enum SwitchPairStatus { + /// Switches are not enabled. + #[default] + Paused, + /// Switches are enabled. + Running, +} + +// Constructor impls +impl SwitchPairInfo { + pub(crate) fn from_input_unchecked( + NewSwitchPairInfo { + pool_account, + remote_asset_circulating_supply, + remote_asset_ed, + remote_asset_id, + remote_asset_total_supply, + remote_xcm_fee, + remote_reserve_location, + status, + }: NewSwitchPairInfo, + ) -> Self { + let remote_asset_sovereign_total_balance = + remote_asset_total_supply.saturating_sub(remote_asset_circulating_supply); + Self { + pool_account, + remote_asset_circulating_supply, + remote_asset_ed, + remote_asset_id, + remote_asset_sovereign_total_balance, + remote_asset_total_supply, + remote_xcm_fee, + remote_reserve_location, + status, + } + } +} + +// Access impls +impl SwitchPairInfo { + pub(crate) fn is_enabled(&self) -> bool { + matches!(self.status, SwitchPairStatus::Running) + } + + /// Returns the balance that the chain effectively has available for swaps + /// on destination. This keeps into account the ED of the remote asset on + /// the remote reserve location. This is the only way that the remote + /// balance should be inspected. + pub(crate) fn reducible_remote_balance(&self) -> u128 { + self.remote_asset_sovereign_total_balance + .saturating_sub(self.remote_asset_ed) + } +} + +// Modify impls +impl SwitchPairInfo { + pub(crate) fn try_process_incoming_switch(&mut self, amount: u128) -> Result<(), ()> { + let new_remote_asset_sovereign_total_balance = self + .remote_asset_sovereign_total_balance + .checked_add(amount) + .ok_or(())?; + let new_circulating_supply = self.remote_asset_circulating_supply.checked_sub(amount).ok_or(())?; + + self.remote_asset_sovereign_total_balance = new_remote_asset_sovereign_total_balance; + self.remote_asset_circulating_supply = new_circulating_supply; + + Ok(()) + } + + pub(crate) fn try_process_outgoing_switch(&mut self, amount: u128) -> Result<(), ()> { + let new_remote_asset_sovereign_total_balance = self + .remote_asset_sovereign_total_balance + .checked_sub(amount) + .ok_or(())?; + let new_circulating_supply = self.remote_asset_circulating_supply.checked_add(amount).ok_or(())?; + + self.remote_asset_sovereign_total_balance = new_remote_asset_sovereign_total_balance; + self.remote_asset_circulating_supply = new_circulating_supply; + + Ok(()) + } +} diff --git a/pallets/pallet-asset-switch/src/tests/force_set_switch_pair.rs b/pallets/pallet-asset-switch/src/tests/force_set_switch_pair.rs new file mode 100644 index 000000000..fbafc64a9 --- /dev/null +++ b/pallets/pallet-asset-switch/src/tests/force_set_switch_pair.rs @@ -0,0 +1,432 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok}; +use frame_system::RawOrigin; +use sp_runtime::{ + traits::{One, Zero}, + DispatchError, +}; + +use crate::{ + mock::{get_asset_hub_location, get_remote_erc20_asset_id, ExtBuilder, MockRuntime, System, XCM_ASSET_FEE}, + switch::SwitchPairStatus, + Event, NewSwitchPairInfoOf, Pallet, SwitchPair, SwitchPairInfoOf, +}; + +#[test] +fn successful() { + let pool_account_address = + Pallet::::pool_account_id_for_remote_asset(&get_remote_erc20_asset_id().into()).unwrap(); + // Case where all issuance is circulating supply requires the same balance (+ED) + // for the pool account + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::force_set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + // Need to leave 1 on this chain for ED, so `MAX - 1` can at most be exchanged back (and transferred + // out from the pool account). + (u64::MAX - 1) as u128, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: (u64::MAX - 1) as u128, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + // Unit balance since we had to leave ED on this chain and no min balance + // requirement (ED) is set for the remote asset. + assert!(switch_pair.unwrap().reducible_remote_balance().is_one()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: (u64::MAX - 1) as u128, + remote_asset_ed: 0, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // Case where all issuance is locked and controlled by our sovereign account. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), 1, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::force_set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + 0, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + // Max balance since all circulating supply is controlled by us. + assert_eq!(switch_pair.unwrap().reducible_remote_balance(), u64::MAX as u128); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // Case where all issuance is circulating supply and there's a min balance >= + // `0` on the remote chain requires the same balance (+ED) for the pool account, + // and the remote balance is calculated accordingly. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::force_set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + (u64::MAX - 1) as u128, + Box::new(get_asset_hub_location().into()), + 1, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: (u64::MAX - 1) as u128, + remote_asset_ed: 1, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + // Zero balance since we everything but the required remote asset ED is + // circulating. + assert!(switch_pair.unwrap().reducible_remote_balance().is_zero()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: (u64::MAX - 1) as u128, + remote_asset_ed: 1, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // Case where all issuance is locked and controlled by our sovereign account, + // but there's a min balance >= `0` on the remote chain. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), 1, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::force_set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + 0, + Box::new(get_asset_hub_location().into()), + 1, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 1, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + // We cannot go below `1` on the remote chain, so of all the locked assets we + // control, we can only exchange all but one. + assert_eq!(switch_pair.unwrap().reducible_remote_balance(), (u64::MAX - 1) as u128); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 1, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); +} + +#[test] +fn successful_overwrites_existing_pool() { + let pool_account_address = + Pallet::::pool_account_id_for_remote_asset(&get_remote_erc20_asset_id().into()).unwrap(); + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), 1, 0, 0)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: Default::default(), + }) + .build() + // We ignore try-runtime tests here since we are testing the breaking of invariants. + .execute_with(|| { + assert_ok!(Pallet::::force_set_switch_pair( + RawOrigin::Root.into(), + 100_000, + Box::new(get_remote_erc20_asset_id().into()), + 50_000, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 50_000, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + assert_eq!(switch_pair.unwrap().reducible_remote_balance(), 50_000); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 50_000, + remote_asset_ed: 0, + remote_asset_total_supply: 100_000, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); +} + +#[test] +fn fails_on_invalid_origin() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::force_set_switch_pair( + RawOrigin::None.into(), + 100_000, + Box::new(get_remote_erc20_asset_id().into()), + 1_000, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn fails_on_invalid_supply_values() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::force_set_switch_pair( + RawOrigin::None.into(), + // Total supply less than locked supply + 1_000, + Box::new(get_remote_erc20_asset_id().into()), + 1_001, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn successful_on_not_enough_funds_on_pool_balance() { + let pool_account_address = + Pallet::::pool_account_id_for_remote_asset(&get_remote_erc20_asset_id().into()).unwrap(); + // It works if not enough free balance is available + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX - 1, 0, 0)]) + .build() + // We ignore try-runtime tests here since we are testing the breaking of invariants. + .execute_with(|| { + assert_ok!(Pallet::::force_set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + u64::MAX as u128, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ),); + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: u64::MAX as u128, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + assert!(switch_pair.unwrap().reducible_remote_balance().is_zero()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: u64::MAX as u128, + remote_asset_ed: 0, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // It works if balance is frozen. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX, 1, 0)]) + .build() + // We ignore try-runtime tests here since we are testing the breaking of invariants. + .execute_with(|| { + assert_ok!(Pallet::::force_set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + u64::MAX as u128, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ),); + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: u64::MAX as u128, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + assert!(switch_pair.unwrap().reducible_remote_balance().is_zero()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: u64::MAX as u128, + remote_asset_ed: 0, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // It works if balance is held. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX, 0, 1)]) + .build() + // We ignore try-runtime tests here since we are testing the breaking of invariants. + .execute_with(|| { + assert_ok!(Pallet::::force_set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + u64::MAX as u128, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ),); + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: u64::MAX as u128, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + assert!(switch_pair.unwrap().reducible_remote_balance().is_zero()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: u64::MAX as u128, + remote_asset_ed: 0, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); +} diff --git a/pallets/pallet-asset-switch/src/tests/force_unset_switch_pair.rs b/pallets/pallet-asset-switch/src/tests/force_unset_switch_pair.rs new file mode 100644 index 000000000..542e7ac2d --- /dev/null +++ b/pallets/pallet-asset-switch/src/tests/force_unset_switch_pair.rs @@ -0,0 +1,71 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok}; +use frame_system::RawOrigin; +use sp_runtime::DispatchError; + +use crate::{ + mock::{get_asset_hub_location, get_remote_erc20_asset_id, ExtBuilder, MockRuntime, System, XCM_ASSET_FEE}, + Event, NewSwitchPairInfoOf, Pallet, SwitchPair, +}; + +#[test] +fn successful() { + // Deletes and generates an event if there is a pool + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: Default::default(), + }) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::force_unset_switch_pair(RawOrigin::Root.into())); + assert!(SwitchPair::::get().is_none()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairRemoved { + remote_asset_id: get_remote_erc20_asset_id().into(), + } + .into())); + }); + // Deletes and generates no event if there is no pool + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::force_unset_switch_pair(RawOrigin::Root.into())); + assert!(SwitchPair::::get().is_none()); + assert!(System::events().into_iter().map(|e| e.event).all(|e| e + != Event::::SwitchPairRemoved { + remote_asset_id: get_remote_erc20_asset_id().into(), + } + .into())); + }); +} + +#[test] +fn fails_on_invalid_origin() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::force_unset_switch_pair(RawOrigin::None.into()), + DispatchError::BadOrigin, + ); + }); +} diff --git a/pallets/pallet-asset-switch/src/tests/mod.rs b/pallets/pallet-asset-switch/src/tests/mod.rs new file mode 100644 index 000000000..71a3ffd2f --- /dev/null +++ b/pallets/pallet-asset-switch/src/tests/mod.rs @@ -0,0 +1,25 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod force_set_switch_pair; +mod force_unset_switch_pair; +mod pause_switch_pair; +mod resume_switch_pair; +mod set_switch_pair; +mod switch; +mod update_remote_xcm_fee; diff --git a/pallets/pallet-asset-switch/src/tests/pause_switch_pair.rs b/pallets/pallet-asset-switch/src/tests/pause_switch_pair.rs new file mode 100644 index 000000000..79708ac36 --- /dev/null +++ b/pallets/pallet-asset-switch/src/tests/pause_switch_pair.rs @@ -0,0 +1,99 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok}; +use frame_system::RawOrigin; +use sp_runtime::DispatchError; + +use crate::{ + mock::{get_asset_hub_location, get_remote_erc20_asset_id, ExtBuilder, MockRuntime, System, XCM_ASSET_FEE}, + switch::SwitchPairStatus, + Error, Event, NewSwitchPairInfoOf, Pallet, SwitchPair, +}; + +#[test] +fn successful() { + // Stopping a running switch pair generates an event. + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::pause_switch_pair(RawOrigin::Root.into())); + assert_eq!( + SwitchPair::::get().unwrap().status, + SwitchPairStatus::Paused + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairPaused { + remote_asset_id: get_remote_erc20_asset_id().into() + } + .into())); + }); + // Stopping a non-running switch pair generates no event. + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::pause_switch_pair(RawOrigin::Root.into())); + assert_eq!( + SwitchPair::::get().unwrap().status, + SwitchPairStatus::Paused + ); + assert!(System::events().into_iter().map(|e| e.event).all(|e| e + != Event::::SwitchPairPaused { + remote_asset_id: get_remote_erc20_asset_id().into() + } + .into())); + }); +} + +#[test] +fn fails_on_non_existing_pair() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::pause_switch_pair(RawOrigin::Root.into()), + Error::::SwitchPairNotFound + ); + }); +} + +#[test] +fn fails_on_invalid_origin() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::pause_switch_pair(RawOrigin::None.into()), + DispatchError::BadOrigin + ); + }); +} diff --git a/pallets/pallet-asset-switch/src/tests/resume_switch_pair.rs b/pallets/pallet-asset-switch/src/tests/resume_switch_pair.rs new file mode 100644 index 000000000..b294cb58e --- /dev/null +++ b/pallets/pallet-asset-switch/src/tests/resume_switch_pair.rs @@ -0,0 +1,99 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok}; +use frame_system::RawOrigin; +use sp_runtime::DispatchError; + +use crate::{ + mock::{get_asset_hub_location, get_remote_erc20_asset_id, ExtBuilder, MockRuntime, System, XCM_ASSET_FEE}, + switch::SwitchPairStatus, + Error, Event, NewSwitchPairInfoOf, Pallet, SwitchPair, +}; + +#[test] +fn successful() { + // Resuming a non-running switch pair generates an event. + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::resume_switch_pair(RawOrigin::Root.into())); + assert_eq!( + SwitchPair::::get().unwrap().status, + SwitchPairStatus::Running + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairResumed { + remote_asset_id: get_remote_erc20_asset_id().into() + } + .into())); + }); + // Resuming a running switch pair generates no event. + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::resume_switch_pair(RawOrigin::Root.into())); + assert_eq!( + SwitchPair::::get().unwrap().status, + SwitchPairStatus::Running + ); + assert!(System::events().into_iter().map(|e| e.event).all(|e| e + != Event::::SwitchPairResumed { + remote_asset_id: get_remote_erc20_asset_id().into() + } + .into())); + }); +} + +#[test] +fn fails_on_non_existing_pair() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::resume_switch_pair(RawOrigin::Root.into()), + Error::::SwitchPairNotFound + ); + }); +} + +#[test] +fn fails_on_invalid_origin() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::resume_switch_pair(RawOrigin::None.into()), + DispatchError::BadOrigin + ); + }); +} diff --git a/pallets/pallet-asset-switch/src/tests/set_switch_pair.rs b/pallets/pallet-asset-switch/src/tests/set_switch_pair.rs new file mode 100644 index 000000000..31a65f544 --- /dev/null +++ b/pallets/pallet-asset-switch/src/tests/set_switch_pair.rs @@ -0,0 +1,393 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok}; +use frame_system::RawOrigin; +use sp_runtime::{ + traits::{One, Zero}, + DispatchError, +}; + +use crate::{ + mock::{get_asset_hub_location, get_remote_erc20_asset_id, ExtBuilder, MockRuntime, System, XCM_ASSET_FEE}, + switch::SwitchPairStatus, + Error, Event, NewSwitchPairInfoOf, Pallet, SwitchPair, SwitchPairInfoOf, +}; + +#[test] +fn successful() { + let pool_account_address = + Pallet::::pool_account_id_for_remote_asset(&get_remote_erc20_asset_id().into()).unwrap(); + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), 1_001, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + 1_000, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 1_000, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + assert_eq!( + switch_pair.unwrap().reducible_remote_balance(), + (u64::MAX - 1_000) as u128 + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 1_000, + remote_asset_ed: 0, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // Case where all issuance is circulating supply requires the same balance (+ED) + // for the pool account + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + // Need to leave 1 on this chain for ED, so `MAX - 1` can at most be exchanged back (and transferred + // out from the pool account). + (u64::MAX - 1) as u128, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: (u64::MAX - 1) as u128, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + // Unit balance since we had to leave ED on this chain + assert!(switch_pair.unwrap().reducible_remote_balance().is_one()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: (u64::MAX - 1) as u128, + remote_asset_ed: 0, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // Case where all issuance is locked and controlled by our sovereign account. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), 1, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + 0, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + // Max balance since all circulating supply is controlled by us and we used `0` + // as the remote asset ED. + assert_eq!(switch_pair.unwrap().reducible_remote_balance(), u64::MAX as u128); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // Case where all issuance is circulating supply and there's a min balance >= + // `0` on the remote chain requires the same balance (+ED) for the pool account, + // and the remote balance is calculated accordingly. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + // Need to leave 1 on this chain for ED, so `MAX - 1` can at most be exchanged back (and transferred + // out from the pool account). + (u64::MAX - 1) as u128, + Box::new(get_asset_hub_location().into()), + // The `1` remaining is used to cover our ED for the remote asset on the remote location. + 1, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: (u64::MAX - 1) as u128, + remote_asset_ed: 1, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + // Zero balance since we everything but the required remote asset ED is + // circulating. + assert!(switch_pair.unwrap().reducible_remote_balance().is_zero()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: (u64::MAX - 1) as u128, + remote_asset_ed: 1, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); + // Case where all issuance is locked and controlled by our sovereign account, + // but there's a min balance >= `0` on the remote chain. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), 1, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + 0, + Box::new(get_asset_hub_location().into()), + 1, + Box::new(XCM_ASSET_FEE.into()), + )); + + let switch_pair = SwitchPair::::get(); + let expected_switch_pair = + SwitchPairInfoOf::::from_input_unchecked(NewSwitchPairInfoOf:: { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 1, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: u64::MAX as u128, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }); + assert_eq!(switch_pair, Some(expected_switch_pair)); + assert_eq!(switch_pair.unwrap().reducible_remote_balance(), (u64::MAX - 1) as u128); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairCreated { + pool_account: pool_account_address.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 1, + remote_asset_total_supply: u64::MAX as u128, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: Box::new(XCM_ASSET_FEE.into()) + } + .into())); + }); +} + +#[test] +fn fails_on_invalid_origin() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::set_switch_pair( + RawOrigin::None.into(), + 100_000, + Box::new(get_remote_erc20_asset_id().into()), + 1_000, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn fails_on_pool_existing() { + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: Default::default(), + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::set_switch_pair( + RawOrigin::Root.into(), + 100_000, + Box::new(get_remote_erc20_asset_id().into()), + 1_000, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ), + Error::::SwitchPairAlreadyExisting + ); + }); +} + +#[test] +fn fails_on_invalid_supply_values() { + // Circulating supply > total issuance + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::set_switch_pair( + RawOrigin::Root.into(), + // Total supply less than locked supply + 1_000, + Box::new(get_remote_erc20_asset_id().into()), + 1_001, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ), + Error::::InvalidInput + ); + }); + // Circulating supply - total issuance < remote ED + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::set_switch_pair( + RawOrigin::Root.into(), + // Total supply equal to locked supply... + 1_000, + Box::new(get_remote_erc20_asset_id().into()), + 1_000, + Box::new(get_asset_hub_location().into()), + // ... but with a required `1` unit to be left at all times + 1, + Box::new(XCM_ASSET_FEE.into()), + ), + Error::::InvalidInput + ); + }); +} + +#[test] +fn fails_on_not_enough_funds_on_pool_balance() { + let pool_account_address = + Pallet::::pool_account_id_for_remote_asset(&get_remote_erc20_asset_id().into()).unwrap(); + // Does not work if not enough free balance is available + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX - 1, 0, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + u64::MAX as u128, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ), + Error::::PoolInitialLiquidityRequirement + ); + }); + // Does not work if balance is frozen. + ExtBuilder::default() + .with_balances(vec![(pool_account_address.clone(), u64::MAX, 1, 0)]) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + u64::MAX as u128, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ), + Error::::PoolInitialLiquidityRequirement + ); + }); + // Does not work if balance is held. + ExtBuilder::default() + .with_balances(vec![(pool_account_address, u64::MAX, 0, 1)]) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::set_switch_pair( + RawOrigin::Root.into(), + u64::MAX as u128, + Box::new(get_remote_erc20_asset_id().into()), + u64::MAX as u128, + Box::new(get_asset_hub_location().into()), + 0, + Box::new(XCM_ASSET_FEE.into()), + ), + Error::::PoolInitialLiquidityRequirement + ); + }); +} diff --git a/pallets/pallet-asset-switch/src/tests/switch.rs b/pallets/pallet-asset-switch/src/tests/switch.rs new file mode 100644 index 000000000..8c95256f0 --- /dev/null +++ b/pallets/pallet-asset-switch/src/tests/switch.rs @@ -0,0 +1,473 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + assert_noop, assert_ok, + traits::fungible::{Inspect, InspectFreeze, InspectHold}, +}; +use frame_system::RawOrigin; +use sp_runtime::{ + traits::{One, TryConvert, Zero}, + AccountId32, DispatchError, +}; +use xcm::v4::{Asset, Fungibility}; + +use crate::{ + mock::{ + get_asset_hub_location, get_remote_erc20_asset_id, Balances, ExtBuilder, MockFungibleAssetTransactor, + MockRuntime, System, FREEZE_REASON, HOLD_REASON, XCM_ASSET_FEE, + }, + switch::SwitchPairStatus, + xcm::convert::AccountId32ToAccountId32JunctionConverter, + Error, Event, NewSwitchPairInfoOf, Pallet, SwitchPair, +}; + +#[test] +fn successful() { + let user = AccountId32::from([0; 32]); + let pool_account = AccountId32::from([1; 32]); + // It works with entire balance unfrozen and un-held. + ExtBuilder::default() + .with_balances(vec![(user.clone(), 100_000, 0, 0), (pool_account.clone(), 1, 0, 0)]) + .with_fungibles(vec![(user.clone(), XCM_ASSET_FEE)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: pool_account.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + let total_currency_issuance_before = >::total_issuance(); + assert_ok!(Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + // Cannot switch ED (1 in the mock), so we need to exclude that. + 99_999, + Box::new(get_asset_hub_location().into()) + )); + let total_currency_issuance_after = >::total_issuance(); + // Total issuance of currency has not changed + assert_eq!(total_currency_issuance_after, total_currency_issuance_before); + // User's currency balance is reduced by switch amount + assert!(>::total_balance(&user).is_one()); + // User's frozen balance has remained unchanged. + assert!(>::balance_frozen(&FREEZE_REASON, &user).is_zero()); + // User's held balance has remained unchanged. + assert!(>::balance_on_hold(&HOLD_REASON, &user).is_zero()); + // Pool's currency balance (previously only ED) is increased by switch amount + assert_eq!( + >::total_balance(&pool_account), + 100_000 + ); + // Pool's remote balance is decreased by switch amount + assert!(SwitchPair::::get() + .unwrap() + .reducible_remote_balance() + .is_one()); + // User's fungible balance is reduced by XCM fee + assert!(MockFungibleAssetTransactor::get_balance_for( + &AccountId32ToAccountId32JunctionConverter::try_convert(user.clone()) + .unwrap() + .into() + ) + .is_zero()); + // Pool's fungible balance is not changed (we're testing that fees are burnt and + // not transferred). + assert!(MockFungibleAssetTransactor::get_balance_for( + &AccountId32ToAccountId32JunctionConverter::try_convert(pool_account.clone()) + .unwrap() + .into() + ) + .is_zero()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::LocalToRemoteSwitchExecuted { + amount: 99_999, + from: user.clone(), + to: get_asset_hub_location().into() + } + .into())); + }); + // It works with balance partially frozen. + ExtBuilder::default() + .with_balances(vec![(user.clone(), 100_000, 1, 0), (pool_account.clone(), 1, 0, 0)]) + .with_fungibles(vec![(user.clone(), XCM_ASSET_FEE)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: pool_account.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + let total_currency_issuance_before = >::total_issuance(); + assert_ok!(Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + 99_999, + Box::new(get_asset_hub_location().into()) + )); + let total_currency_issuance_after = >::total_issuance(); + // Total issuance of currency has not changed + assert_eq!(total_currency_issuance_after, total_currency_issuance_before); + // User's currency balance is reduced by switch amount + assert!(>::total_balance(&user).is_one()); + // User's frozen balance has remained unchanged. + assert!(>::balance_frozen(&FREEZE_REASON, &user).is_one()); + // User's held balance has remained unchanged. + assert!(>::balance_on_hold(&HOLD_REASON, &user).is_zero()); + // Pool's currency balance (previously only ED) is increased by switch amount + assert_eq!( + >::total_balance(&pool_account), + 100_000 + ); + // Pool's remote balance is decreased by switch amount + assert!(SwitchPair::::get() + .unwrap() + .reducible_remote_balance() + .is_one()); + // User's fungible balance is reduced by XCM fee + assert!(MockFungibleAssetTransactor::get_balance_for( + &AccountId32ToAccountId32JunctionConverter::try_convert(user.clone()) + .unwrap() + .into() + ) + .is_zero()); + // Pool's fungible balance is not changed (we're testing that fees are burnt and + // not transferred). + assert!(MockFungibleAssetTransactor::get_balance_for( + &AccountId32ToAccountId32JunctionConverter::try_convert(pool_account.clone()) + .unwrap() + .into() + ) + .is_zero()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::LocalToRemoteSwitchExecuted { + amount: 99_999, + from: user.clone(), + to: get_asset_hub_location().into() + } + .into())); + }); + // It works with balance partially held. + ExtBuilder::default() + // Free balance not allowed to go to zero. + .with_balances(vec![(user.clone(), 100_001, 0, 1), (pool_account.clone(), 1, 0, 0)]) + .with_fungibles(vec![(user.clone(), XCM_ASSET_FEE)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: pool_account.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + let total_currency_issuance_before = >::total_issuance(); + assert_ok!(Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + 99_999, + Box::new(get_asset_hub_location().into()) + )); + let total_currency_issuance_after = >::total_issuance(); + // Total issuance of currency has not changed + assert_eq!(total_currency_issuance_after, total_currency_issuance_before); + // User's currency balance is reduced by switch amount + assert_eq!(>::total_balance(&user), 2); + // User's frozen balance has remained unchanged. + assert!(>::balance_frozen(&FREEZE_REASON, &user).is_zero()); + // User's held balance has remained unchanged. + assert!(>::balance_on_hold(&HOLD_REASON, &user).is_one()); + // Pool's currency balance (previously only ED) is increased by switch amount + assert_eq!( + >::total_balance(&pool_account), + 100_000 + ); + // Pool's remote balance is decreased by switch amount + assert!(SwitchPair::::get() + .unwrap() + .reducible_remote_balance() + .is_one()); + // User's fungible balance is reduced by XCM fee + assert!(MockFungibleAssetTransactor::get_balance_for( + &AccountId32ToAccountId32JunctionConverter::try_convert(user.clone()) + .unwrap() + .into() + ) + .is_zero()); + // Pool's fungible balance is not changed (we're testing that fees are burnt and + // not transferred). + assert!(MockFungibleAssetTransactor::get_balance_for( + &AccountId32ToAccountId32JunctionConverter::try_convert(pool_account.clone()) + .unwrap() + .into() + ) + .is_zero()); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::LocalToRemoteSwitchExecuted { + amount: 99_999, + from: user.clone(), + to: get_asset_hub_location().into() + } + .into())); + }); +} + +#[test] +fn fails_on_invalid_origin() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch(RawOrigin::Root.into(), 1, Box::new(get_asset_hub_location().into())), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn fails_on_non_existing_pool() { + let user = AccountId32::from([0; 32]); + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user).into(), + 1, + Box::new(get_asset_hub_location().into()) + ), + Error::::SwitchPairNotFound + ); + }); +} + +#[test] +fn fails_on_pool_not_running() { + let user = AccountId32::from([0; 32]); + let pool_account = AccountId32::from([1; 32]); + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account, + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user).into(), + 1, + Box::new(get_asset_hub_location().into()) + ), + Error::::SwitchPairNotEnabled + ); + }); +} + +#[test] +fn fails_on_not_enough_user_local_balance() { + let user = AccountId32::from([0; 32]); + let pool_account = AccountId32::from([1; 32]); + // Fails if user has not enough balance. + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: pool_account.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + 100_000, + Box::new(get_asset_hub_location().into()) + ), + Error::::UserSwitchBalance + ); + }); + // Fails if user has frozen balance. + ExtBuilder::default() + .with_balances(vec![(user.clone(), 100_000, 1, 0)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: pool_account.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + 100_000, + Box::new(get_asset_hub_location().into()) + ), + Error::::UserSwitchBalance + ); + }); + // Fails if user has held balance. + ExtBuilder::default() + .with_balances(vec![(user.clone(), 100_000, 0, 1)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: pool_account.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + 100_000, + Box::new(get_asset_hub_location().into()) + ), + Error::::UserSwitchBalance + ); + }); + // Fails if user goes under their ED. + ExtBuilder::default() + .with_balances(vec![(user.clone(), 100_000, 0, 0)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account, + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user).into(), + 100_000, + Box::new(get_asset_hub_location().into()) + ), + Error::::UserSwitchBalance + ); + }); +} + +#[test] +fn fails_on_not_enough_remote_balance() { + let user = AccountId32::from([0; 32]); + let pool_account = AccountId32::from([1; 32]); + // Case where min remote balance is `0` + ExtBuilder::default() + .with_balances(vec![(user.clone(), 100_000, 0, 1)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: pool_account.clone(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 50_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + 50_001, + Box::new(get_asset_hub_location().into()) + ), + Error::::Liquidity + ); + }); + // Case where min remote balance is `1` + ExtBuilder::default() + .with_balances(vec![(user.clone(), 100_000, 0, 1)]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account, + remote_asset_circulating_supply: 0, + remote_asset_ed: 1, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 50_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + // Tradeable are only 49_999 because of the remote ED. + 50_000, + Box::new(get_asset_hub_location().into()) + ), + Error::::Liquidity + ); + }); +} + +#[test] +fn fails_on_not_enough_user_xcm_balance() { + let user = AccountId32::from([0; 32]); + let pool_account = AccountId32::from([1; 32]); + ExtBuilder::default() + .with_balances(vec![(user.clone(), 100_000, 0, 1)]) + .with_fungibles(vec![( + user.clone(), + Asset { + // 1 unit less than required + fun: Fungibility::Fungible(999), + ..XCM_ASSET_FEE + }, + )]) + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account, + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 100_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Running, + }) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::switch( + RawOrigin::Signed(user.clone()).into(), + 50_001, + Box::new(get_asset_hub_location().into()) + ), + Error::::UserXcmBalance + ); + }); +} diff --git a/pallets/pallet-asset-switch/src/tests/update_remote_xcm_fee.rs b/pallets/pallet-asset-switch/src/tests/update_remote_xcm_fee.rs new file mode 100644 index 000000000..6098ed42f --- /dev/null +++ b/pallets/pallet-asset-switch/src/tests/update_remote_xcm_fee.rs @@ -0,0 +1,111 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok}; +use frame_system::RawOrigin; +use sp_runtime::DispatchError; +use xcm::v4::{Asset, Fungibility}; + +use crate::{ + mock::{get_asset_hub_location, get_remote_erc20_asset_id, ExtBuilder, MockRuntime, System, XCM_ASSET_FEE}, + Error, Event, NewSwitchPairInfoOf, Pallet, SwitchPair, SwitchPairStatus, +}; + +#[test] +fn successful() { + // Setting the fee to a new value generates an event. + let new_fee = Asset { + fun: Fungibility::Fungible(1), + ..XCM_ASSET_FEE + }; + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: Default::default(), + }) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::update_remote_xcm_fee( + RawOrigin::Root.into(), + Box::new(new_fee.clone().into()) + )); + assert_eq!( + SwitchPair::::get().unwrap().remote_xcm_fee, + new_fee.clone().into() + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::SwitchPairFeeUpdated { + old: XCM_ASSET_FEE.into(), + new: new_fee.clone().into() + } + .into())); + }); + // Setting the fee to the same value does not generate an event. + ExtBuilder::default() + .with_switch_pair_info(NewSwitchPairInfoOf:: { + pool_account: [0u8; 32].into(), + remote_asset_circulating_supply: 0, + remote_asset_ed: 0, + remote_asset_id: get_remote_erc20_asset_id().into(), + remote_asset_total_supply: 1_000, + remote_reserve_location: get_asset_hub_location().into(), + remote_xcm_fee: XCM_ASSET_FEE.into(), + status: SwitchPairStatus::Paused, + }) + .build_and_execute_with_sanity_tests(|| { + assert_ok!(Pallet::::update_remote_xcm_fee( + RawOrigin::Root.into(), + Box::new(XCM_ASSET_FEE.into()) + )); + assert_eq!( + SwitchPair::::get().unwrap().remote_xcm_fee, + XCM_ASSET_FEE.into() + ); + assert!(System::events().into_iter().map(|e| e.event).all(|e| e + != Event::::SwitchPairFeeUpdated { + old: XCM_ASSET_FEE.into(), + new: XCM_ASSET_FEE.into(), + } + .into())); + }); +} + +#[test] +fn fails_on_invalid_origin() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::update_remote_xcm_fee(RawOrigin::None.into(), Box::new(XCM_ASSET_FEE.into()),), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn fails_on_non_existing_switch_pair() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + Pallet::::update_remote_xcm_fee(RawOrigin::Root.into(), Box::new(XCM_ASSET_FEE.into()),), + Error::::SwitchPairNotFound + ); + }); +} diff --git a/pallets/pallet-asset-switch/src/traits.rs b/pallets/pallet-asset-switch/src/traits.rs new file mode 100644 index 000000000..30606d92d --- /dev/null +++ b/pallets/pallet-asset-switch/src/traits.rs @@ -0,0 +1,87 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use xcm::VersionedLocation; + +use crate::{Config, LocalCurrencyBalanceOf}; + +/// Runtime-injected logic into the switch pallet around the time a switch takes +/// place. +pub trait SwitchHooks +where + T: Config, + I: 'static, +{ + type Error: Into; + + /// Called before anything related to a switch happens. + fn pre_local_to_remote_switch( + from: &T::AccountId, + to: &VersionedLocation, + amount: LocalCurrencyBalanceOf, + ) -> Result<(), Self::Error>; + + /// Called after the switch takes place and **after** the XCM message has + /// been sent to the reserve location. + fn post_local_to_remote_switch( + from: &T::AccountId, + to: &VersionedLocation, + amount: LocalCurrencyBalanceOf, + ) -> Result<(), Self::Error>; + + /// Called upon receiving an XCM message from the reserve location to + /// deposit some of the remote assets into a specified account, but before + /// the asset is actually deposited by the asset transactor. + fn pre_remote_to_local_switch(to: &T::AccountId, amount: u128) -> Result<(), Self::Error>; + + /// Same as [Self::pre_remote_to_local_switch], but called after the + /// transactor has deposited the incoming remote asset. + fn post_remote_to_local_switch(to: &T::AccountId, amount: u128) -> Result<(), Self::Error>; +} + +impl SwitchHooks for () +where + T: Config, + I: 'static, +{ + type Error = u8; + + fn pre_local_to_remote_switch( + _from: &T::AccountId, + _to: &VersionedLocation, + _amount: LocalCurrencyBalanceOf, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn post_local_to_remote_switch( + _from: &T::AccountId, + _to: &VersionedLocation, + _amount: LocalCurrencyBalanceOf, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn pre_remote_to_local_switch(_to: &::AccountId, _amount: u128) -> Result<(), Self::Error> { + Ok(()) + } + + fn post_remote_to_local_switch(_to: &::AccountId, _amount: u128) -> Result<(), Self::Error> { + Ok(()) + } +} diff --git a/pallets/pallet-asset-switch/src/try_state.rs b/pallets/pallet-asset-switch/src/try_state.rs new file mode 100644 index 000000000..8843ab675 --- /dev/null +++ b/pallets/pallet-asset-switch/src/try_state.rs @@ -0,0 +1,65 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::ensure; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::TryRuntimeError; +use sp_std::cmp::Ordering; + +use crate::{Config, LocalCurrencyBalanceOf, Pallet, SwitchPair}; + +const LOG_TARGET: &str = "try-state::pallet-asset-switch"; + +pub(crate) fn do_try_state(_n: BlockNumberFor) -> Result<(), TryRuntimeError> +where + T: Config, + I: 'static, + LocalCurrencyBalanceOf: Into, +{ + let Some(switch_pair) = SwitchPair::::get() else { + return Ok(()); + }; + // At all times, the circulating supply must be entirely covered by the + // reducible balance of the pool account. + ensure!( + switch_pair.remote_asset_circulating_supply + <= Pallet::::get_pool_reducible_balance(&switch_pair.pool_account).into(), + TryRuntimeError::Other("Circulating supply less than the switch pool account.") + ); + // At all times, the total (reducible + ED) balance of the remote sovereign + // account must be smaller than the (total - circulating) supply. Ideally, these + // should be equal, but there are cases of trapped assets in which equality does + // not hold + let stored_remote_balance = switch_pair.reducible_remote_balance() + switch_pair.remote_asset_ed; + let locked_balance_from_total_and_circulating = + switch_pair.remote_asset_total_supply - switch_pair.remote_asset_circulating_supply; + + match stored_remote_balance.cmp(&locked_balance_from_total_and_circulating) { + Ordering::Less => { + log::warn!(target: LOG_TARGET, "Stored remote balance {:?} does not strictly equal expected balance from total and circulating supply ({:?} - {:?} = {:?}", stored_remote_balance, switch_pair.remote_asset_total_supply, switch_pair.remote_asset_circulating_supply, locked_balance_from_total_and_circulating); + Ok(()) + } + Ordering::Greater => { + log::error!(target: LOG_TARGET, "Stored remote balance {:?} must never exceed the expected balance from total and circulating supply ({:?} - {:?} = {:?}", stored_remote_balance, switch_pair.remote_asset_total_supply, switch_pair.remote_asset_circulating_supply, locked_balance_from_total_and_circulating); + Err(TryRuntimeError::Other( + "Tracked locked balance greater than calculated locked supply.", + )) + } + Ordering::Equal => Ok(()), + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/convert/mod.rs b/pallets/pallet-asset-switch/src/xcm/convert/mod.rs new file mode 100644 index 000000000..40e7eef5e --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/convert/mod.rs @@ -0,0 +1,36 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use sp_runtime::{traits::TryConvert, AccountId32}; +use xcm::v4::Junction::{self, AccountId32 as AccountId32Junction}; + +const LOG_TARGET: &str = "xcm::pallet-asset-switch::AccountId32ToAccountId32JunctionConverter"; + +/// Type implementing `TryConvert` and returns a +/// `Junction` from an `AccountId32`. +pub struct AccountId32ToAccountId32JunctionConverter; + +impl TryConvert for AccountId32ToAccountId32JunctionConverter { + fn try_convert(account: AccountId32) -> Result { + log::info!(target: LOG_TARGET, "try_convert {:?}", account); + Ok(AccountId32Junction { + network: None, + id: account.into(), + }) + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/match/mock.rs b/pallets/pallet-asset-switch/src/xcm/match/mock.rs new file mode 100644 index 000000000..ce0168b25 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/match/mock.rs @@ -0,0 +1,156 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + construct_runtime, + traits::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, + Everything, + }, +}; +use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned}; +use pallet_balances::AccountData; +use sp_core::{ConstU16, ConstU32, ConstU64, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, CheckedConversion, IdentityLookup}, + AccountId32, +}; + +use crate::{NewSwitchPairInfoOf, Pallet, SwitchPairInfoOf}; + +construct_runtime!( + pub enum MockRuntime { + System: frame_system, + Balances: pallet_balances, + Assetswitch: crate + } +); + +impl frame_system::Config for MockRuntime { + type AccountData = AccountData; + type AccountId = AccountId32; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = ConstU64<0>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<1>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeTask = (); + type SS58Prefix = ConstU16<0>; + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for MockRuntime { + type AccountStore = System; + type Balance = u64; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<0>; + type MaxLocks = ConstU32<0>; + type MaxReserves = ConstU32<0>; + type ReserveIdentifier = (); + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = (); + type RuntimeHoldReason = (); + type WeightInfo = (); +} + +impl crate::Config for MockRuntime { + type AccountIdConverter = (); + type AssetTransactor = (); + type FeeOrigin = EnsureRoot; + type LocalCurrency = Balances; + type PauseOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SubmitterOrigin = EnsureSigned; + type SwitchHooks = (); + type SwitchOrigin = EnsureRoot; + type XcmRouter = (); + type WeightInfo = (); + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +#[derive(Default)] +pub(super) struct ExtBuilder(Option>); + +impl ExtBuilder { + pub(super) fn with_switch_pair_info(mut self, switch_pair_info: NewSwitchPairInfoOf) -> Self { + self.0 = Some(switch_pair_info); + self + } + + pub(super) fn build(self) -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + System::set_block_number(1); + + if let Some(switch_pair_info) = self.0 { + let switch_pair_info = SwitchPairInfoOf::::from_input_unchecked(switch_pair_info); + + // Set pool balance to local ED + circulating supply, to maintain + // invariants and make them verifiable. + let local_ed = >::minimum_balance(); + >::mint_into( + &switch_pair_info.pool_account, + local_ed + u64::checked_from(switch_pair_info.remote_asset_circulating_supply).unwrap(), + ) + .unwrap(); + Pallet::::set_switch_pair_bypass_checks( + switch_pair_info.remote_asset_total_supply, + switch_pair_info.remote_asset_id, + switch_pair_info.remote_asset_circulating_supply, + switch_pair_info.remote_reserve_location, + switch_pair_info.remote_asset_ed, + switch_pair_info.remote_xcm_fee, + switch_pair_info.pool_account, + ); + Pallet::::set_switch_pair_status(switch_pair_info.status).unwrap(); + } + + System::reset_events() + }); + + ext + } + + pub(super) fn build_and_execute_with_sanity_tests(self, run: impl FnOnce()) { + let mut ext = self.build(); + ext.execute_with(|| { + run(); + crate::try_state::do_try_state::(System::block_number()).unwrap(); + }); + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/match/mod.rs b/pallets/pallet-asset-switch/src/xcm/match/mod.rs new file mode 100644 index 000000000..6374ae667 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/match/mod.rs @@ -0,0 +1,83 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::ensure; +use sp_std::marker::PhantomData; +use xcm::v4::{Asset, AssetId, Fungibility, Location}; +use xcm_executor::traits::{Error as XcmExecutorError, MatchesFungibles}; + +use crate::{Config, SwitchPair}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +const LOG_TARGET: &str = "xcm::pallet-asset-switch::MatchesSwitchPairXcmFeeFungibleAsset"; + +/// Type implementing [MatchesFungibles] and returns the provided +/// fungible amount if the specified `Location` matches the asset used by +/// the switch pallet to pay for XCM fees at the configured remote location +/// (`switch_pair_info.remote_xcm_fee`). +pub struct MatchesSwitchPairXcmFeeFungibleAsset(PhantomData<(T, I)>); + +impl MatchesFungibles for MatchesSwitchPairXcmFeeFungibleAsset +where + T: Config, + I: 'static, + FungiblesBalance: From, +{ + fn matches_fungibles(a: &Asset) -> Result<(Location, FungiblesBalance), XcmExecutorError> { + log::info!(target: LOG_TARGET, "matches_fungibles {:?}", a); + // 1. Retrieve switch pair from storage. + let switch_pair = SwitchPair::::get().ok_or(XcmExecutorError::AssetNotHandled)?; + + // 2. Ensure switch pair is enabled + ensure!(switch_pair.is_enabled(), XcmExecutorError::AssetNotHandled); + + // 3. Match stored asset ID with input asset ID. + let Asset { id, fun } = switch_pair.remote_xcm_fee.clone().try_into().map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert stored remote fee asset {:?} into v4 Location with error {:?}.", + switch_pair.remote_xcm_fee, + e + ); + XcmExecutorError::AssetNotHandled + })?; + ensure!(id == a.id, XcmExecutorError::AssetNotHandled); + // 4. Verify the stored asset is a fungible one. + let Fungibility::Fungible(_) = fun else { + log::info!(target: LOG_TARGET, "Stored remote fee asset {:?} is not a fungible one.", switch_pair.remote_xcm_fee); + return Err(XcmExecutorError::AssetNotHandled); + }; + + // After this ensure, we know we need to be transacting with this asset, so any + // errors thrown from here onwards is a `FailedToTransactAsset` error. + + let AssetId(location) = id; + // 5. Force input asset as a fungible one and return its amount. + let Fungibility::Fungible(amount) = a.fun else { + log::info!(target: LOG_TARGET, "Input asset {:?} is supposed to be fungible but it is not.", a); + return Err(XcmExecutorError::AmountToBalanceConversionFailed); + }; + + log::trace!(target: LOG_TARGET, "matched {:?}", (location.clone(), amount)); + Ok((location, amount.into())) + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/match/tests.rs b/pallets/pallet-asset-switch/src/xcm/match/tests.rs new file mode 100644 index 000000000..960639e40 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/match/tests.rs @@ -0,0 +1,260 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::assert_noop; +use xcm::{ + v4::{Asset, AssetId, AssetInstance, Fungibility, Junction, Junctions, Location}, + IntoVersion, VersionedAsset, +}; +use xcm_executor::traits::{Error, MatchesFungibles}; + +use crate::{ + xcm::{ + r#match::mock::{ExtBuilder, MockRuntime}, + test_utils::get_switch_pair_info_for_remote_location, + MatchesSwitchPairXcmFeeFungibleAsset, + }, + SwitchPairStatus, +}; + +#[test] +fn successful_with_stored_latest() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let (asset_location, asset_amount): (Location, u128) = + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(location.clone()), + fun: Fungibility::Fungible(u128::MAX), + }) + .unwrap(); + // Asset location should match the one stored in the switch pair. + assert_eq!(asset_location, location); + // Asset amount should match the input one. + assert_eq!(asset_amount, u128::MAX); + }); +} + +#[test] +fn successful_with_stored_v4() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let (asset_location, asset_amount): (Location, u128) = + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(location.clone()), + fun: Fungibility::Fungible(u128::MAX), + }) + .unwrap(); + // Asset location should match the one stored in the switch pair. + assert_eq!(asset_location, location); + // Asset amount should match the input one. + assert_eq!(asset_amount, u128::MAX); + }); +} + +#[test] +fn successful_with_stored_v3() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let location_v4: Location = location.try_into().unwrap(); + + let (asset_location, asset_amount): (Location, u128) = + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(location_v4.clone()), + fun: Fungibility::Fungible(u128::MAX), + }) + .unwrap(); + // Asset location should match the one stored in the switch pair. + assert_eq!(asset_location, location_v4); + // Asset amount should match the input one. + assert_eq!(asset_amount, u128::MAX); + }); +} + +#[test] +fn successful_with_stored_v2() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let new_switch_pair_info = { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to an XCM v2. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(2).unwrap(); + new_switch_pair_info + }; + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let (asset_location, asset_amount): (Location, u128) = + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(location_v3.try_into().unwrap()), + fun: Fungibility::Fungible(u128::MAX), + }) + .unwrap(); + // Asset location should match the one stored in the switch pair. + assert_eq!(asset_location, location_v3.try_into().unwrap()); + // Asset amount should match the input one. + assert_eq!(asset_amount, u128::MAX); + }); +} + +#[test] +fn skips_on_switch_pair_not_set() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + assert_noop!( + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }), + fun: Fungibility::Fungible(u128::MAX), + }) as Result<(_, u128), _>, + Error::AssetNotHandled + ); + }); +} + +#[test] +fn skips_on_switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Paused); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(location), + fun: Fungibility::Fungible(u128::MAX), + }) as Result<(_, u128), _>, + Error::AssetNotHandled + ); + }); +} + +#[test] +fn skips_on_different_asset() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let different_location = Location { + parents: 1, + // Different para ID. + interior: Junctions::X1([Junction::Parachain(1_001)].into()), + }; + assert_noop!( + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(different_location), + fun: Fungibility::Fungible(u128::MAX), + }) as Result<(_, u128), _>, + Error::AssetNotHandled + ); + }); +} + +#[test] +fn skips_on_non_fungible_stored_asset() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let non_fungible_asset_amount = Fungibility::NonFungible(AssetInstance::Index(1)); + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to one with a non-fungible amount. + new_switch_pair_info.remote_xcm_fee = VersionedAsset::V4(Asset { + id: AssetId(location.clone()), + fun: non_fungible_asset_amount, + }); + new_switch_pair_info + }; + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(location), + fun: Fungibility::Fungible(u128::MAX), + }) as Result<(_, u128), _>, + Error::AssetNotHandled + ); + }); +} + +#[test] +fn fails_on_non_fungible_input_asset() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + assert_noop!( + MatchesSwitchPairXcmFeeFungibleAsset::::matches_fungibles(&Asset { + id: AssetId(location), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }) as Result<(_, u128), _>, + Error::AmountToBalanceConversionFailed + ); + }); +} diff --git a/pallets/pallet-asset-switch/src/xcm/mod.rs b/pallets/pallet-asset-switch/src/xcm/mod.rs new file mode 100644 index 000000000..5e99a20a1 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/mod.rs @@ -0,0 +1,35 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +pub mod convert; +pub use convert::AccountId32ToAccountId32JunctionConverter; + +pub mod r#match; +pub use r#match::MatchesSwitchPairXcmFeeFungibleAsset; + +pub mod transfer; +pub use transfer::{IsSwitchPairRemoteAsset, IsSwitchPairXcmFeeAsset}; + +pub mod trade; +pub use trade::{UsingComponentsForSwitchPairRemoteAsset, UsingComponentsForXcmFeeAsset}; + +pub mod transact; +pub use transact::SwitchPairRemoteAssetTransactor; + +#[cfg(test)] +mod test_utils; diff --git a/pallets/pallet-asset-switch/src/xcm/test_utils.rs b/pallets/pallet-asset-switch/src/xcm/test_utils.rs new file mode 100644 index 000000000..d8695ba87 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/test_utils.rs @@ -0,0 +1,60 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use sp_runtime::traits::Zero; +use xcm::{ + v4::{Asset, AssetId, Fungibility, Location}, + VersionedAsset, VersionedAssetId, VersionedLocation, +}; + +use crate::{Config, NewSwitchPairInfoOf, SwitchPairStatus}; + +pub(super) fn get_switch_pair_info_for_remote_location_with_pool_usable_balance( + location: &Location, + pool_usable_balance: u64, + status: SwitchPairStatus, +) -> NewSwitchPairInfoOf +where + Runtime: Config, + Runtime::AccountId: From<[u8; 32]>, +{ + NewSwitchPairInfoOf:: { + pool_account: Runtime::AccountId::from([1; 32]), + remote_asset_id: VersionedAssetId::V4(AssetId(location.clone())), + remote_reserve_location: VersionedLocation::V4(location.clone()), + remote_xcm_fee: VersionedAsset::V4(Asset { + id: AssetId(location.clone()), + fun: Fungibility::Fungible(1), + }), + remote_asset_total_supply: (u64::MAX as u128) + pool_usable_balance as u128, + remote_asset_circulating_supply: pool_usable_balance as u128, + remote_asset_ed: u128::zero(), + status, + } +} + +pub(super) fn get_switch_pair_info_for_remote_location( + location: &Location, + status: SwitchPairStatus, +) -> NewSwitchPairInfoOf +where + Runtime: Config, + Runtime::AccountId: From<[u8; 32]>, +{ + get_switch_pair_info_for_remote_location_with_pool_usable_balance::(location, 0, status) +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/mod.rs b/pallets/pallet-asset-switch/src/xcm/trade/mod.rs new file mode 100644 index 000000000..8002e7fe1 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/mod.rs @@ -0,0 +1,26 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod xcm_fee_asset; +pub use xcm_fee_asset::UsingComponentsForXcmFeeAsset; + +mod switch_pair_remote_asset; +pub use switch_pair_remote_asset::UsingComponentsForSwitchPairRemoteAsset; + +#[cfg(test)] +mod test_utils; diff --git a/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/mock.rs b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/mock.rs new file mode 100644 index 000000000..9af584b1e --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/mock.rs @@ -0,0 +1,166 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + construct_runtime, + traits::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, + Everything, + }, +}; +use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned}; +use pallet_balances::AccountData; +use sp_core::{ConstU16, ConstU32, ConstU64, Get, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, CheckedConversion, IdentityLookup}, + AccountId32, +}; + +use crate::{NewSwitchPairInfoOf, Pallet, SwitchPairInfoOf}; + +construct_runtime!( + pub enum MockRuntime { + System: frame_system, + Balances: pallet_balances, + Assetswitch: crate + } +); + +impl frame_system::Config for MockRuntime { + type AccountData = AccountData; + type AccountId = AccountId32; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = ConstU64<0>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<1>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeTask = (); + type SS58Prefix = ConstU16<0>; + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for MockRuntime { + type AccountStore = System; + type Balance = u64; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<0>; + type MaxLocks = ConstU32<0>; + type MaxReserves = ConstU32<0>; + type ReserveIdentifier = (); + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = (); + type RuntimeHoldReason = (); + type WeightInfo = (); +} + +impl crate::Config for MockRuntime { + type AccountIdConverter = (); + type AssetTransactor = (); + type FeeOrigin = EnsureRoot; + type LocalCurrency = Balances; + type PauseOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SubmitterOrigin = EnsureSigned; + type SwitchHooks = (); + type SwitchOrigin = EnsureRoot; + type XcmRouter = (); + type WeightInfo = (); + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +pub(super) const DESTINATION_ACCOUNT: AccountId32 = AccountId32::new([100; 32]); +#[derive(Debug, Clone)] +pub(super) struct ToDestinationAccount; + +impl Get for ToDestinationAccount { + fn get() -> AccountId32 { + DESTINATION_ACCOUNT + } +} + +#[derive(Default)] +pub(super) struct ExtBuilder(Option>); + +impl ExtBuilder { + pub(super) fn with_switch_pair_info(mut self, switch_pair_info: NewSwitchPairInfoOf) -> Self { + self.0 = Some(switch_pair_info); + self + } + + pub(super) fn build(self) -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + System::set_block_number(1); + + if let Some(switch_pair_info) = self.0 { + let switch_pair_info = SwitchPairInfoOf::::from_input_unchecked(switch_pair_info); + + // Set pool balance to local ED + circulating supply, to maintain + // invariants and make them verifiable. + let local_ed = >::minimum_balance(); + >::mint_into( + &switch_pair_info.pool_account, + local_ed + u64::checked_from(switch_pair_info.remote_asset_circulating_supply).unwrap(), + ) + .unwrap(); + Pallet::::set_switch_pair_bypass_checks( + switch_pair_info.remote_asset_total_supply, + switch_pair_info.remote_asset_id, + switch_pair_info.remote_asset_circulating_supply, + switch_pair_info.remote_reserve_location, + switch_pair_info.remote_asset_ed, + switch_pair_info.remote_xcm_fee, + switch_pair_info.pool_account, + ); + Pallet::::set_switch_pair_status(switch_pair_info.status).unwrap(); + } + + System::reset_events() + }); + + ext + } + + pub(super) fn build_and_execute_with_sanity_tests(self, run: impl FnOnce()) { + let mut ext = self.build(); + ext.execute_with(|| { + run(); + crate::try_state::do_try_state::(System::block_number()).unwrap(); + }); + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/mod.rs b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/mod.rs new file mode 100644 index 000000000..57651e0d0 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/mod.rs @@ -0,0 +1,266 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + ensure, + traits::{fungible::Mutate, tokens::Preservation}, + weights::WeightToFee as WeightToFeeT, +}; +use sp_core::Get; +use sp_runtime::traits::Zero; +use sp_std::marker::PhantomData; +use xcm::v4::{Asset, AssetId, Error, Weight, XcmContext, XcmHash}; +use xcm_executor::{traits::WeightTrader, AssetsInHolding}; + +use crate::{Config, LocalCurrencyBalanceOf, SwitchPair, SwitchPairInfoOf}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +const LOG_TARGET: &str = "xcm::pallet-asset-switch::UsingComponentsForSwitchPairRemoteAsset"; + +/// Type implementing [WeightTrader] that allows paying for XCM fees when +/// reserve transferring the remote asset of the on-chain switch pair. +/// +/// This trader is required in case there is no other mechanism to pay for +/// fees when transferring such an asset to this chain. +/// +/// Any unused fee is transferred from the switch pair pool account to the +/// specified account. +#[derive(Default, Debug, Clone)] +pub struct UsingComponentsForSwitchPairRemoteAsset +where + T: Config, + I: 'static, + FeeDestinationAccount: Get, +{ + remaining_weight: Weight, + remaining_fungible_balance: u128, + consumed_xcm_hash: Option, + switch_pair: Option>, + _phantom: PhantomData<(WeightToFee, I, FeeDestinationAccount)>, +} + +impl PartialEq + for UsingComponentsForSwitchPairRemoteAsset +where + T: Config, + I: 'static, + FeeDestinationAccount: Get, +{ + fn eq(&self, other: &Self) -> bool { + self.remaining_weight == other.remaining_weight + && self.remaining_fungible_balance == other.remaining_fungible_balance + && self.consumed_xcm_hash == other.consumed_xcm_hash + && self.switch_pair == other.switch_pair + } +} + +impl WeightTrader + for UsingComponentsForSwitchPairRemoteAsset +where + T: Config, + I: 'static, + FeeDestinationAccount: Get, + + WeightToFee: WeightToFeeT, +{ + fn new() -> Self { + let switch_pair = SwitchPair::::get(); + Self { + consumed_xcm_hash: None, + remaining_fungible_balance: Zero::zero(), + remaining_weight: Zero::zero(), + switch_pair, + _phantom: PhantomData, + } + } + + fn buy_weight( + &mut self, + weight: Weight, + payment: AssetsInHolding, + context: &XcmContext, + ) -> Result { + log::info!( + target: LOG_TARGET, + "buy_weight {:?}, {:?}, {:?}", + weight, + payment, + context + ); + + // Prevent re-using the same trader more than once. + ensure!(self.consumed_xcm_hash.is_none(), Error::NotWithdrawable); + // Asset not relevant if no switch pair is set or if not enabled. + let switch_pair = self.switch_pair.as_ref().ok_or(Error::AssetNotFound)?; + ensure!(switch_pair.is_enabled(), Error::AssetNotFound); + + let amount = WeightToFee::weight_to_fee(&weight); + + let switch_pair_remote_asset_v4: AssetId = switch_pair.remote_asset_id.clone().try_into().map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert stored asset ID {:?} into v4 AssetId with error {:?}", + switch_pair.remote_asset_id, + e + ); + Error::FailedToTransactAsset("Failed to convert switch pair asset ID into required version.") + })?; + + let required: Asset = (switch_pair_remote_asset_v4, amount).into(); + let unused = payment.checked_sub(required.clone()).map_err(|_| Error::TooExpensive)?; + + // Set link to XCM message ID only if this is the trader used. + log::trace!(target: LOG_TARGET, "Required {:?} - unused {:?}", required, unused); + self.consumed_xcm_hash = Some(context.message_id); + self.remaining_fungible_balance = self.remaining_fungible_balance.saturating_add(amount); + self.remaining_weight = self.remaining_weight.saturating_add(weight); + + Ok(unused) + } + + fn refund_weight(&mut self, weight: Weight, context: &XcmContext) -> Option { + log::trace!( + target: LOG_TARGET, + "UsingComponents::refund_weight weight: {:?}, context: {:?}", + weight, + context + ); + + // Ensure we refund in the same trader we took fees from. + if Some(context.message_id) != self.consumed_xcm_hash { + return None; + }; + + let Some(ref switch_pair) = self.switch_pair else { + log::error!(target: LOG_TARGET, "Stored switch pair should not be None, but it is."); + return None; + }; + if !switch_pair.is_enabled() { + return None; + } + + let switch_pair_remote_asset_v4: AssetId = switch_pair + .remote_asset_id + .clone() + .try_into() + .map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert stored asset ID {:?} into v4 AssetId with error {:?}", + switch_pair.remote_asset_id, + e + ); + Error::FailedToTransactAsset("Failed to convert switch pair asset ID into required version.") + }) + .ok()?; + + let weight_to_refund: Weight = weight.min(self.remaining_weight); + let amount_for_weight_to_refund = WeightToFee::weight_to_fee(&weight_to_refund); + // We can only refund up to the remaining balance of this weigher. + let amount_to_refund = amount_for_weight_to_refund.min(self.remaining_fungible_balance); + + self.consumed_xcm_hash = None; + self.remaining_fungible_balance = self.remaining_fungible_balance.saturating_sub(amount_to_refund); + self.remaining_weight = self.remaining_weight.saturating_sub(weight_to_refund); + + if amount_to_refund > 0 { + log::trace!( + target: LOG_TARGET, + "Refund amount {:?}", + (switch_pair_remote_asset_v4.clone(), amount_to_refund) + ); + Some((switch_pair_remote_asset_v4, amount_to_refund).into()) + } else { + log::trace!(target: LOG_TARGET, "No refund"); + None + } + } +} + +// Move any unused asset from the switch pool account to the specified account, +// and update the remote balance with the difference since we know we control +// the full amount on the remote location. +impl Drop + for UsingComponentsForSwitchPairRemoteAsset +where + T: Config, + I: 'static, + FeeDestinationAccount: Get, +{ + fn drop(&mut self) { + log::trace!( + target: LOG_TARGET, + "Drop with remaining {:?}", + ( + self.consumed_xcm_hash, + self.remaining_fungible_balance, + self.remaining_weight, + &self.switch_pair + ) + ); + + // Nothing to refund if this trader was not called or if the leftover balance is + // zero. + if let Some(switch_pair) = &self.switch_pair { + // We don't care if the pool is enabled, since we're sending all non-refunded + // weight to the configured destination account (e.g., treasury). + if self.remaining_fungible_balance > Zero::zero() { + let Ok(remaining_balance_as_local_currency) = LocalCurrencyBalanceOf::::try_from(self.remaining_fungible_balance).map_err(|e| { + log::error!(target: LOG_TARGET, "Failed to convert remaining balance {:?} to local currency balance", self.remaining_fungible_balance); + e + }) else { return; }; + + // No error should ever be thrown from inside this block. + let transfer_result = >::transfer( + &switch_pair.pool_account, + &FeeDestinationAccount::get(), + remaining_balance_as_local_currency, + Preservation::Preserve, + ).map_err(|e| { + log::error!(target: LOG_TARGET, "Failed to transfer unused balance {:?} from switch pair pool account {:?} to specified account {:?}", remaining_balance_as_local_currency, switch_pair.pool_account, FeeDestinationAccount::get()); + e + }); + + debug_assert!( + transfer_result.is_ok(), + "Transferring from pool account to fee destination failed." + ); + + // No error should ever be thrown from inside this block. + SwitchPair::::mutate(|entry| { + let Some(entry) = entry.as_mut() else { + log::error!(target: LOG_TARGET, "Stored switch pair should not be None but it is."); + return; + }; + entry + .try_process_incoming_switch(self.remaining_fungible_balance) + .unwrap_or_else(|_| { + log::error!( + target: LOG_TARGET, + "Failed to increase balance of remote sovereign account due to overflow." + ); + }); + }); + } + } + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/buy_weight.rs b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/buy_weight.rs new file mode 100644 index 000000000..f12a45049 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/buy_weight.rs @@ -0,0 +1,574 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_storage_noop}; +use xcm::{ + v4::{Asset, AssetId, AssetInstance, Error, Fungibility, Junction, Junctions, Location, Weight, XcmContext}, + IntoVersion, +}; +use xcm_executor::{traits::WeightTrader, AssetsInHolding}; + +use crate::{ + xcm::{ + test_utils::get_switch_pair_info_for_remote_location_with_pool_usable_balance, + trade::{ + switch_pair_remote_asset::mock::{ExtBuilder, MockRuntime, ToDestinationAccount}, + test_utils::SumTimeAndProofValues, + }, + UsingComponentsForSwitchPairRemoteAsset, + }, + SwitchPairStatus, +}; + +#[test] +fn successful_on_stored_remote_asset_latest_with_input_fungible() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 2, SwitchPairStatus::Running); + // Set remote asset to the latest XCM version. + new_switch_pair_info.remote_asset_id = new_switch_pair_info.remote_asset_id.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }] + .into(); + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + assert!(unused_weight.is_empty()); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn successful_on_stored_remote_asset_latest_with_input_non_fungible() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 2, SwitchPairStatus::Running); + // Set remote asset to the latest XCM version. + new_switch_pair_info.remote_asset_id = new_switch_pair_info.remote_asset_id.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn successful_on_stored_remote_asset_latest_with_input_fungible_and_non_fungible() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 2, SwitchPairStatus::Running); + // Set remote asset to the latest XCM version. + new_switch_pair_info.remote_asset_id = new_switch_pair_info.remote_asset_id.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![ + Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }, + Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }, + ] + .into(); + + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + // The non-fungible asset is left in the registry. + assert_eq!( + unused_weight, + vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + },] + .into() + ); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v4_with_input_fungible() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 3, + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }] + .into(); + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + assert!(unused_weight.is_empty()); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v4_with_input_non_fungible() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 3, + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v4_with_input_fungible_and_non_fungible() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 3, + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![ + Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }, + Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }, + ] + .into(); + + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + // The non-fungible asset is left in the registry. + assert_eq!( + unused_weight, + vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + },] + .into() + ); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v3_with_input_fungible() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location.try_into().unwrap(), + 3, + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }] + .into(); + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + assert!(unused_weight.is_empty()); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v3_with_input_non_fungible() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location.try_into().unwrap(), + 3, + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v3_with_input_fungible_and_non_fungible() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + // Give to pool amount same amount that is being purchased in the test case + + // ED. + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location.try_into().unwrap(), + 3, + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![ + Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }, + Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }, + ] + .into(); + + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + // The non-fungible asset is left in the registry. + assert_eq!( + unused_weight, + vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + },] + .into() + ); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn fails_on_rerun() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 0, + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.consumed_xcm_hash = Some([0; 32]); + weigher + }; + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::NotWithdrawable + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_switch_pair_not_set() { + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: AssetId(Location::here()), + fun: Fungibility::Fungible(1), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 1, + SwitchPairStatus::Paused, + ); + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn fails_on_too_expensive() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 0, + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + // Using only `1` asset is not sufficient. + fun: Fungibility::Fungible(1), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/drop.rs b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/drop.rs new file mode 100644 index 000000000..47ae640e0 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/drop.rs @@ -0,0 +1,179 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_storage_noop, traits::fungible::Inspect as InspectFungible}; +use sp_core::Get; +use sp_runtime::{ + traits::{One, Zero}, + AccountId32, +}; +use xcm::v4::{Junction, Junctions, Location}; +use xcm_executor::traits::WeightTrader; + +use crate::{ + xcm::{ + test_utils::get_switch_pair_info_for_remote_location_with_pool_usable_balance, + trade::{ + switch_pair_remote_asset::mock::{Balances, ExtBuilder, MockRuntime, ToDestinationAccount}, + test_utils::SumTimeAndProofValues, + }, + UsingComponentsForSwitchPairRemoteAsset, + }, + SwitchPairStatus, +}; + +#[test] +fn happy_path() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + // ED + 1 + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 1, + SwitchPairStatus::Running, + ); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = 1; + weigher + }; + assert!(>::balance(&ToDestinationAccount::get()).is_zero()); + drop(weigher); + assert!(>::balance(&ToDestinationAccount::get()).is_one()); + assert!(>::balance(&new_switch_pair_info.pool_account).is_one()); + }); +} + +#[test] +fn no_switch_pair() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + let weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = 1; + weigher + }; + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 1, + SwitchPairStatus::Paused, + ); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = 1; + weigher + }; + assert!(>::balance(&ToDestinationAccount::get()).is_zero()); + drop(weigher); + assert!(>::balance(&ToDestinationAccount::get()).is_one()); + assert!(>::balance(&new_switch_pair_info.pool_account).is_one()); + }); +} + +#[test] +fn zero_remaining_balance() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 0, + SwitchPairStatus::Running, + ); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher + }; + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +#[should_panic(expected = "Transferring from pool account to fee destination failed.")] +fn fail_to_transfer_from_pool_account() { + // Same setup as the happy path, minus the balance set for the pool. + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 0, + SwitchPairStatus::Running, + ); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = 1; + weigher + }; + assert!(>::balance(&ToDestinationAccount::get()).is_zero()); + drop(weigher); + assert!(>::balance(&ToDestinationAccount::get()).is_one()); + assert!(>::balance(&new_switch_pair_info.pool_account).is_zero()); + }); +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/mod.rs b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/mod.rs new file mode 100644 index 000000000..0bd72041a --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/mod.rs @@ -0,0 +1,21 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod buy_weight; +mod drop; +mod refund_weight; diff --git a/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/refund_weight.rs b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/refund_weight.rs new file mode 100644 index 000000000..8070c322d --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/switch_pair_remote_asset/tests/refund_weight.rs @@ -0,0 +1,682 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use sp_runtime::traits::Zero; +use xcm::{ + v4::{Asset, Fungibility, Junction, Junctions, Location, Weight, XcmContext}, + IntoVersion, +}; +use xcm_executor::traits::WeightTrader; + +use crate::{ + xcm::{ + test_utils::get_switch_pair_info_for_remote_location_with_pool_usable_balance, + trade::{ + switch_pair_remote_asset::mock::{ExtBuilder, MockRuntime, ToDestinationAccount}, + test_utils::SumTimeAndProofValues, + }, + UsingComponentsForSwitchPairRemoteAsset, + }, + SwitchPairStatus, +}; + +#[test] +fn successful_on_stored_remote_asset_latest_with_remaining_balance_and_weight() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 0, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!( + amount_refunded, + Some(Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2) + }) + ); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX - 2); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_latest_with_zero_remaining_balance() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 0, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // No balance is refunded, weight is. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_latest_with_zero_remaining_weight() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 0, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded, remaining balance is not changed. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_latest_with_zero_remaining_balance_and_weight() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 0, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v4_with_remaining_balance_and_weight() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 0, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!( + amount_refunded, + Some(Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2) + }) + ); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX - 2); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v4_with_zero_remaining_balance() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 0, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // No balance is refunded, weight is. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v4_with_zero_remaining_weight() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 0, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded, remaining balance is not changed. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v4_with_zero_remaining_balance_and_weight() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 0, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v3_with_remaining_balance_and_weight() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location.try_into().unwrap(), 0, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!( + amount_refunded, + Some(Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2) + }) + ); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX - 2); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v3_with_zero_remaining_balance() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location.try_into().unwrap(), 0, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // No balance is refunded, weight is. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v3_with_zero_remaining_weight() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location.try_into().unwrap(), 0, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded, remaining balance is not changed. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_remote_asset_v3_with_zero_remaining_balance_and_weight() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location.try_into().unwrap(), 0, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn skips_on_weight_not_previously_purchased() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 0, + SwitchPairStatus::Running, + ); + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Fails with XCM message hash `None`. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + // Setting this to 'None' triggers the "not bought with me" condition. + weigher.consumed_xcm_hash = None; + weigher + }; + let initial_weigher = weigher.clone(); + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert!(amount_refunded.is_none()); + assert_eq!(initial_weigher, weigher); + }); +} + +#[test] +fn skips_on_weight_not_previously_purchased_different_hash() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 0, + SwitchPairStatus::Running, + ); + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Fails with XCM message hash `Some(something_else)`. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + // Setting this to a different value than expected also triggers the "not bought + // with me" condition. + weigher.consumed_xcm_hash = Some([100; 32]); + weigher + }; + let initial_weigher = weigher.clone(); + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert!(amount_refunded.is_none()); + assert_eq!(initial_weigher, weigher); + }); +} + +#[test] +fn skips_on_switch_pair_not_set() { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + let mut weigher = { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let initial_weigher = weigher.clone(); + let amount_refunded = weigher.refund_weight(Weight::from_parts(1, 1), &XcmContext::with_message_id([0u8; 32])); + assert!(amount_refunded.is_none()); + assert_eq!(initial_weigher, weigher); + }); +} + +#[test] +fn skips_on_switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 0, + SwitchPairStatus::Paused, + ); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let mut weigher = UsingComponentsForSwitchPairRemoteAsset::< + MockRuntime, + _, + SumTimeAndProofValues, + ToDestinationAccount, + >::new(); + let initial_weigher = weigher.clone(); + let amount_refunded = + weigher.refund_weight(Weight::from_parts(1, 1), &XcmContext::with_message_id([0u8; 32])); + assert!(amount_refunded.is_none()); + assert_eq!(initial_weigher, weigher); + }); +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/test_utils.rs b/pallets/pallet-asset-switch/src/xcm/trade/test_utils.rs new file mode 100644 index 000000000..6d1e5cecf --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/test_utils.rs @@ -0,0 +1,31 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::weights::WeightToFee; +use xcm::v3::Weight; + +#[derive(Debug, Clone)] +pub(super) struct SumTimeAndProofValues; + +impl WeightToFee for SumTimeAndProofValues { + type Balance = u128; + + fn weight_to_fee(weight: &Weight) -> Self::Balance { + (weight.ref_time() + weight.proof_size()) as u128 + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mock.rs b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mock.rs new file mode 100644 index 000000000..831f5c392 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mock.rs @@ -0,0 +1,176 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + construct_runtime, + traits::{ + fungible::Dust, + tokens::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible, Unbalanced as UnbalancedFungible}, + DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence, + }, + Everything, + }, +}; +use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned}; +use sp_core::{ConstU16, ConstU32, ConstU64, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, DispatchError, +}; + +use crate::{NewSwitchPairInfoOf, Pallet}; + +construct_runtime!( + pub enum MockRuntime { + System: frame_system, + Assetswitch: crate + } +); + +impl frame_system::Config for MockRuntime { + type AccountData = (); + type AccountId = AccountId32; + type RuntimeTask = (); + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = ConstU64<0>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<1>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = ConstU16<0>; + type SystemWeightInfo = (); + type Version = (); +} + +// Currency is not used in this XCM component tests, so we mock the entire +// currency system. +pub struct MockCurrency; + +impl MutateFungible for MockCurrency {} + +impl InspectFungible for MockCurrency { + type Balance = u64; + + fn active_issuance() -> Self::Balance { + Self::Balance::default() + } + + fn balance(_who: &AccountId32) -> Self::Balance { + Self::Balance::default() + } + + fn can_deposit(_who: &AccountId32, _amount: Self::Balance, _provenance: Provenance) -> DepositConsequence { + DepositConsequence::Success + } + + fn can_withdraw(_who: &AccountId32, _amount: Self::Balance) -> WithdrawConsequence { + WithdrawConsequence::Success + } + + fn minimum_balance() -> Self::Balance { + Self::Balance::default() + } + + fn reducible_balance(_who: &AccountId32, _preservation: Preservation, _force: Fortitude) -> Self::Balance { + Self::Balance::default() + } + + fn total_balance(_who: &AccountId32) -> Self::Balance { + Self::Balance::default() + } + + fn total_issuance() -> Self::Balance { + Self::Balance::default() + } +} + +impl UnbalancedFungible for MockCurrency { + fn handle_dust(_dust: Dust) {} + + fn write_balance(_who: &AccountId32, _amount: Self::Balance) -> Result, DispatchError> { + Ok(Some(Self::Balance::default())) + } + + fn set_total_issuance(_amount: Self::Balance) {} +} + +impl crate::Config for MockRuntime { + type AccountIdConverter = (); + type AssetTransactor = (); + type FeeOrigin = EnsureRoot; + type LocalCurrency = MockCurrency; + type PauseOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SubmitterOrigin = EnsureSigned; + type SwitchHooks = (); + type SwitchOrigin = EnsureRoot; + type XcmRouter = (); + type WeightInfo = (); + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +#[derive(Default)] +pub(super) struct ExtBuilder(Option>); + +impl ExtBuilder { + pub(super) fn with_switch_pair_info(mut self, switch_pair_info: NewSwitchPairInfoOf) -> Self { + self.0 = Some(switch_pair_info); + self + } + + pub(super) fn build(self) -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + System::set_block_number(1); + + if let Some(switch_pair_info) = self.0 { + Pallet::::set_switch_pair_bypass_checks( + switch_pair_info.remote_asset_total_supply, + switch_pair_info.remote_asset_id, + switch_pair_info.remote_asset_circulating_supply, + switch_pair_info.remote_reserve_location, + switch_pair_info.remote_asset_ed, + switch_pair_info.remote_xcm_fee, + switch_pair_info.pool_account, + ); + Pallet::::set_switch_pair_status(switch_pair_info.status).unwrap(); + } + + System::reset_events() + }); + + ext + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mod.rs b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mod.rs new file mode 100644 index 000000000..26ca483b2 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/mod.rs @@ -0,0 +1,212 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ensure, weights::WeightToFee as WeightToFeeT}; +use sp_runtime::traits::Zero; +use sp_std::marker::PhantomData; +use xcm::v4::{Asset, Error, Fungibility, Weight, XcmContext, XcmHash}; +use xcm_executor::{traits::WeightTrader, AssetsInHolding}; + +use crate::{Config, SwitchPair}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +const LOG_TARGET: &str = "xcm::pallet-asset-switch::UsingComponentsForXcmFeeAsset"; + +/// Type implementing [WeightTrader] that allows +/// paying for XCM fees when reserve transferring the XCM fee asset for the +/// on-chain switch pair. +/// +/// This trader is required in case there is no other mechanism to pay for +/// fees when transferring such an asset to this chain. +/// +/// Currently, this trader treats the XCM fee asset as if it were 1:1 with the +/// local currency asset. For cases where the XCM fee asset is considered of +/// greater value than the local currency, this is typically fine. For the other +/// cases, using this trader is not recommended. +#[derive(Default, Debug, Clone)] +pub struct UsingComponentsForXcmFeeAsset +where + T: Config, + I: 'static, +{ + remaining_weight: Weight, + remaining_fungible_balance: u128, + consumed_xcm_hash: Option, + _phantom: PhantomData<(T, I, WeightToFee)>, +} + +impl PartialEq for UsingComponentsForXcmFeeAsset +where + T: Config, + I: 'static, +{ + fn eq(&self, other: &Self) -> bool { + self.remaining_weight == other.remaining_weight + && self.remaining_fungible_balance == other.remaining_fungible_balance + && self.consumed_xcm_hash == other.consumed_xcm_hash + } +} + +impl WeightTrader for UsingComponentsForXcmFeeAsset +where + T: Config, + I: 'static, + + WeightToFee: WeightToFeeT, +{ + fn new() -> Self { + Self { + consumed_xcm_hash: None, + remaining_fungible_balance: Zero::zero(), + remaining_weight: Zero::zero(), + _phantom: PhantomData, + } + } + + fn buy_weight( + &mut self, + weight: Weight, + payment: AssetsInHolding, + context: &XcmContext, + ) -> Result { + log::info!( + target: LOG_TARGET, + "buy_weight {:?}, {:?}, {:?}", + weight, + payment, + context + ); + + // Prevent re-using the same trader more than once. + ensure!(self.consumed_xcm_hash.is_none(), Error::NotWithdrawable); + // Asset not relevant if no switch pair is set, or not enabled. + let switch_pair = SwitchPair::::get().ok_or(Error::AssetNotFound)?; + ensure!(switch_pair.is_enabled(), Error::AssetNotFound); + + let amount = WeightToFee::weight_to_fee(&weight); + + let xcm_fee_asset_v4: Asset = switch_pair.remote_xcm_fee.clone().try_into().map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert stored asset ID {:?} into v4 Asset with error {:?}", + switch_pair.remote_xcm_fee, + e + ); + Error::FailedToTransactAsset("Failed to convert switch pair asset ID into required version.") + })?; + // Asset not relevant if the stored XCM fee asset is not fungible. + let Fungibility::Fungible(_) = xcm_fee_asset_v4.fun else { + log::info!(target: LOG_TARGET, "Stored XCM fee asset is not fungible."); + return Err(Error::AssetNotFound); + }; + + let required: Asset = (xcm_fee_asset_v4.id, amount).into(); + let unused = payment.checked_sub(required.clone()).map_err(|_| Error::TooExpensive)?; + + // Set link to XCM message ID only if this is the trader used. + log::trace!(target: LOG_TARGET, "Required {:?} - unused {:?}", required, unused); + self.consumed_xcm_hash = Some(context.message_id); + self.remaining_fungible_balance = self.remaining_fungible_balance.saturating_add(amount); + self.remaining_weight = self.remaining_weight.saturating_add(weight); + + Ok(unused) + } + + fn refund_weight(&mut self, weight: Weight, context: &XcmContext) -> Option { + log::info!(target: LOG_TARGET, "refund_weight weight: {:?} {:?}", weight, context); + + // Ensure we refund in the same trader we took fees from. + if Some(context.message_id) != self.consumed_xcm_hash { + return None; + }; + + let Some(switch_pair) = SwitchPair::::get() else { + log::error!(target: LOG_TARGET, "Stored switch pair should not be None, but it is."); + return None; + }; + if !switch_pair.is_enabled() { + return None; + } + + let xcm_fee_asset_v4: Asset = switch_pair + .remote_xcm_fee + .clone() + .try_into() + .map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert stored asset ID {:?} into v4 AssetId with error {:?}", + switch_pair.remote_xcm_fee, + e + ); + e + }) + .ok()?; + // Double check the store asset fungibility type, in case it changes between + // weight purchase and weight refund. + let Fungibility::Fungible(_) = xcm_fee_asset_v4.fun else { + log::info!(target: LOG_TARGET, "Stored XCM fee asset is not fungible."); + return None; + }; + + let weight_to_refund: Weight = weight.min(self.remaining_weight); + let amount_for_weight_to_refund = WeightToFee::weight_to_fee(&weight_to_refund); + // We can only refund up to the remaining balance of this weigher. + let amount_to_refund = amount_for_weight_to_refund.min(self.remaining_fungible_balance); + + self.consumed_xcm_hash = None; + self.remaining_fungible_balance = self.remaining_fungible_balance.saturating_sub(amount_to_refund); + self.remaining_weight = self.remaining_weight.saturating_sub(weight_to_refund); + + if amount_to_refund > 0 { + log::trace!( + target: LOG_TARGET, + "Refund amount {:?}", + (xcm_fee_asset_v4.clone().id, amount_to_refund) + ); + + Some((xcm_fee_asset_v4.id, amount_to_refund).into()) + } else { + log::trace!(target: LOG_TARGET, "No refund"); + None + } + } +} + +// We burn whatever surplus we have since we know we control it at destination. +impl Drop for UsingComponentsForXcmFeeAsset +where + T: Config, + I: 'static, +{ + fn drop(&mut self) { + log::trace!( + target: LOG_TARGET, + "Drop with remaining {:?}", + ( + self.consumed_xcm_hash, + self.remaining_fungible_balance, + self.remaining_weight + ) + ); + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/buy_weight.rs b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/buy_weight.rs new file mode 100644 index 000000000..6829dc738 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/buy_weight.rs @@ -0,0 +1,854 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_storage_noop}; +use xcm::{ + v4::{Asset, AssetId, AssetInstance, Error, Fungibility, Junction, Junctions, Location, Weight, XcmContext}, + IntoVersion, +}; +use xcm_executor::{traits::WeightTrader, AssetsInHolding}; + +use crate::{ + xcm::{ + test_utils::get_switch_pair_info_for_remote_location, + trade::{ + test_utils::SumTimeAndProofValues, + xcm_fee_asset::mock::{ExtBuilder, MockRuntime}, + }, + UsingComponentsForXcmFeeAsset, + }, + SwitchPairStatus, +}; + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_latest_with_input_fungible() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }] + .into(); + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + assert!(unused_weight.is_empty()); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn fails_on_stored_fungible_xcm_fee_asset_latest_with_input_non_fungible() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_latest_with_input_fungible_and_non_fungible() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![ + Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }, + Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }, + ] + .into(); + + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + // The non-fungible asset is left in the registry. + assert_eq!( + unused_weight, + vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + },] + .into() + ); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v4_with_input_fungible() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }] + .into(); + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + assert!(unused_weight.is_empty()); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn fails_on_stored_fungible_xcm_fee_asset_v4_with_input_non_fungible() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v4_with_input_fungible_and_non_fungible() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![ + Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }, + Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }, + ] + .into(); + + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + // The non-fungible asset is left in the registry. + assert_eq!( + unused_weight, + vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + },] + .into() + ); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v3_with_input_fungible() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }] + .into(); + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + assert!(unused_weight.is_empty()); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn fails_on_stored_fungible_xcm_fee_asset_v3_with_input_non_fungible() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v3_with_input_fungible_and_non_fungible() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![ + Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }, + Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }, + ] + .into(); + + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + // The non-fungible asset is left in the registry. + assert_eq!( + unused_weight, + vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + },] + .into() + ); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v2_with_input_fungible() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 2. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(2).unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }] + .into(); + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + assert!(unused_weight.is_empty()); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn fails_on_stored_fungible_xcm_fee_asset_v2_with_input_non_fungible() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 2. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(2).unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v2_with_input_fungible_and_non_fungible() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 2. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(2).unwrap(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![ + Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }, + Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }, + ] + .into(); + + let unused_weight = weigher.buy_weight(weight_to_buy, payment, &xcm_context).unwrap(); + // The non-fungible asset is left in the registry. + assert_eq!( + unused_weight, + vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + },] + .into() + ); + assert_eq!(weigher.consumed_xcm_hash, Some(xcm_context.message_id)); + assert_eq!(weigher.remaining_fungible_balance, 2); + assert_eq!(weigher.remaining_weight, weight_to_buy); + }); +} + +#[test] +fn fails_on_rerun() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.consumed_xcm_hash = Some([0; 32]); + weigher + }; + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::NotWithdrawable + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_switch_pair_not_set() { + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default().build().execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: AssetId(Location::here()), + fun: Fungibility::Fungible(1), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Paused); + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_stored_non_fungible_xcm_fee_asset_latest_with_fungible_input() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + let non_fungible_remote_xcm_fee_latest = xcm::latest::Asset::try_from(new_switch_pair_info.remote_xcm_fee) + .map(|asset| xcm::latest::Asset { + id: asset.id, + fun: xcm::latest::Fungibility::NonFungible(xcm::latest::AssetInstance::Index(1)), + }) + .unwrap(); + new_switch_pair_info.remote_xcm_fee = non_fungible_remote_xcm_fee_latest.into(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_stored_non_fungible_xcm_fee_asset_latest_with_non_fungible_input() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + let non_fungible_remote_xcm_fee_latest = xcm::latest::Asset::try_from(new_switch_pair_info.remote_xcm_fee) + .map(|asset| xcm::latest::Asset { + id: asset.id, + fun: xcm::latest::Fungibility::NonFungible(xcm::latest::AssetInstance::Index(1)), + }) + .unwrap(); + new_switch_pair_info.remote_xcm_fee = non_fungible_remote_xcm_fee_latest.into(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_stored_non_fungible_xcm_fee_asset_v4_with_non_fungible_input() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + let non_fungible_remote_xcm_fee_v4 = Asset::try_from(new_switch_pair_info.remote_xcm_fee) + .map(|asset| Asset { + id: asset.id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }) + .unwrap(); + new_switch_pair_info.remote_xcm_fee = non_fungible_remote_xcm_fee_v4.into(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_stored_non_fungible_xcm_fee_asset_v3_with_fungible_input() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 3. + let non_fungible_remote_xcm_fee_v3 = Asset::try_from(new_switch_pair_info.remote_xcm_fee) + .map(|asset| Asset { + id: asset.id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }) + .unwrap(); + new_switch_pair_info.remote_xcm_fee = non_fungible_remote_xcm_fee_v3.into(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_stored_non_fungible_xcm_fee_asset_v2_with_fungible_input() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 2. + let non_fungible_remote_xcm_fee_v2: xcm::v2::MultiAsset = + xcm::v2::MultiAsset::try_from(new_switch_pair_info.remote_xcm_fee) + .map(|asset| xcm::v2::MultiAsset { + id: asset.id, + fun: xcm::v2::Fungibility::NonFungible(xcm::v2::AssetInstance::Index(1)), + }) + .unwrap(); + new_switch_pair_info.remote_xcm_fee = non_fungible_remote_xcm_fee_v2.into(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_stored_non_fungible_xcm_fee_asset_v2_with_non_fungible_input() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 2. + let non_fungible_remote_xcm_fee_v2: xcm::v2::MultiAsset = + xcm::v2::MultiAsset::try_from(new_switch_pair_info.remote_xcm_fee) + .map(|asset| xcm::v2::MultiAsset { + id: asset.id, + fun: xcm::v2::Fungibility::NonFungible(xcm::v2::AssetInstance::Index(1)), + }) + .unwrap(); + new_switch_pair_info.remote_xcm_fee = non_fungible_remote_xcm_fee_v2.into(); + new_switch_pair_info + }; + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::AssetNotFound + ); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn fails_on_too_expensive() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Results in a required amount of `2` local currency tokens. + let weight_to_buy = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + let payment: AssetsInHolding = vec![Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + // Using only `1` asset is not sufficient. + fun: Fungibility::Fungible(1), + }] + .into(); + assert_noop!( + weigher.buy_weight(weight_to_buy, payment, &xcm_context), + Error::TooExpensive + ); + assert_storage_noop!(drop(weigher)); + }); +} diff --git a/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/mod.rs b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/mod.rs new file mode 100644 index 000000000..dd990bd7f --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/mod.rs @@ -0,0 +1,20 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod buy_weight; +mod refund_weight; diff --git a/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/refund_weight.rs b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/refund_weight.rs new file mode 100644 index 000000000..31c24d6e7 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/trade/xcm_fee_asset/tests/refund_weight.rs @@ -0,0 +1,725 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::assert_storage_noop; +use sp_runtime::traits::Zero; +use xcm::{ + v4::{Asset, AssetInstance, Fungibility, Junction, Junctions, Location, Weight, XcmContext}, + IntoVersion, +}; +use xcm_executor::traits::WeightTrader; + +use crate::{ + xcm::{ + test_utils::get_switch_pair_info_for_remote_location, + trade::{ + test_utils::SumTimeAndProofValues, + xcm_fee_asset::mock::{ExtBuilder, MockRuntime}, + }, + UsingComponentsForXcmFeeAsset, + }, + SwitchPairStatus, +}; + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_latest_with_remaining_balance_and_weight() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!( + amount_refunded, + Some(Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2) + }) + ); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX - 2); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_latest_with_zero_remaining_balance() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // No balance is refunded, weight is. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_latest_with_zero_remaining_weight() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded, remaining balance is not changed. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_latest_with_zero_remaining_balance_and_weight() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v4_with_remaining_balance_and_weight() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Case when remaining balance and weight are both higher than refund. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!( + amount_refunded, + Some(Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2) + }) + ); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX - 2); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v4_with_zero_remaining_balance() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // No balance is refunded, weight is. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v4_with_zero_remaining_weight() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded, remaining balance is not changed. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v4_with_zero_remaining_balance_and_weight() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v3_with_remaining_balance_and_weight() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Case when remaining balance and weight are both higher than refund. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!( + amount_refunded, + Some(Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2) + }) + ); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX - 2); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v3_with_zero_remaining_balance() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // No balance is refunded, weight is. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v3_with_zero_remaining_weight() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded, remaining balance is not changed. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v3_with_zero_remaining_balance_and_weight() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Nothing is refunded. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn successful_on_stored_fungible_xcm_fee_asset_v2() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set XCM fee asset to the XCM version 2. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(2).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Case when remaining balance and weight are both higher than refund. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!( + amount_refunded, + Some(Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(2) + }) + ); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX - 2); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); + // Case when remaining balance is 0 -> Nothing is refunded. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert_eq!(weigher.remaining_weight, Weight::MAX - weight_to_refund); + assert!(weigher.consumed_xcm_hash.is_none()); + }); + // Case when remaining weight is 0 -> Nothing is refunded, remaining balance is + // not changed. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert_eq!(weigher.remaining_fungible_balance, u128::MAX); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); + // Case when both remaining weight and remaining balance are 0 -> Nothing is + // refunded. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::zero(); + weigher.remaining_weight = Weight::zero(); + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert_eq!(amount_refunded, None); + assert!(weigher.remaining_fungible_balance.is_zero()); + assert!(weigher.remaining_weight.is_zero()); + assert!(weigher.consumed_xcm_hash.is_none()); + }); +} + +#[test] +fn skips_on_weight_not_previously_purchased() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_version(3).unwrap(); + new_switch_pair_info + }; + // Results in an amount of `2` local currency tokens. + let weight_to_refund = Weight::from_parts(1, 1); + let xcm_context = XcmContext::with_message_id([0u8; 32]); + // Fails with XCM message hash `None`. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + // Setting this to 'None' triggers the "not bought with me" condition. + weigher.consumed_xcm_hash = None; + weigher + }; + let initial_weigher = weigher.clone(); + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert!(amount_refunded.is_none()); + assert_eq!(initial_weigher, weigher); + }); + // Fails with XCM message hash `Some(something_else)`. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + // Setting this to a different value than expected also triggers the "not bought + // with me" condition. + weigher.consumed_xcm_hash = Some([100; 32]); + weigher + }; + let initial_weigher = weigher.clone(); + let amount_refunded = weigher.refund_weight(weight_to_refund, &xcm_context); + assert!(amount_refunded.is_none()); + assert_eq!(initial_weigher, weigher); + }); +} + +#[test] +fn skips_on_switch_pair_not_set() { + ExtBuilder::default().build().execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let initial_weigher = weigher.clone(); + let amount_refunded = weigher.refund_weight(Weight::from_parts(1, 1), &XcmContext::with_message_id([0u8; 32])); + assert!(amount_refunded.is_none()); + assert_eq!(initial_weigher, weigher); + }); +} + +#[test] +fn skips_on_switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Paused); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + assert!(weigher + .refund_weight(Weight::from_parts(1, 1), &XcmContext::with_message_id([0u8; 32])) + .is_none()); + assert_storage_noop!(drop(weigher)); + }); +} + +#[test] +fn skips_on_stored_non_fungible_xcm_fee_asset() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the XCM version 3. + let non_fungible_remote_xcm_fee_v3 = Asset::try_from(new_switch_pair_info.remote_xcm_fee) + .map(|asset| Asset { + id: asset.id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }) + .unwrap(); + new_switch_pair_info.remote_xcm_fee = non_fungible_remote_xcm_fee_v3.into(); + new_switch_pair_info + }; + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build() + .execute_with(|| { + let mut weigher = { + let mut weigher = UsingComponentsForXcmFeeAsset::::new(); + weigher.remaining_fungible_balance = u128::MAX; + weigher.remaining_weight = Weight::MAX; + weigher.consumed_xcm_hash = Some([0u8; 32]); + weigher + }; + let initial_weigher = weigher.clone(); + assert!(weigher + .refund_weight(Weight::from_parts(1, 1), &XcmContext::with_message_id([0u8; 32])) + .is_none()); + assert_eq!(initial_weigher, weigher); + }); +} diff --git a/pallets/pallet-asset-switch/src/xcm/transact/mock.rs b/pallets/pallet-asset-switch/src/xcm/transact/mock.rs new file mode 100644 index 000000000..be1760ff9 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transact/mock.rs @@ -0,0 +1,230 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + construct_runtime, + pallet_prelude::TypeInfo, + traits::{ + fungible::{ + freeze::Mutate as MutateFreeze, hold::Mutate as MutateHold, Inspect as InspectFungible, + Mutate as MutateFungible, + }, + Everything, VariantCount, + }, +}; +use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned}; +use pallet_balances::AccountData; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use sp_core::{ConstU16, ConstU32, ConstU64, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, CheckedConversion, IdentityLookup}, + AccountId32, +}; +use xcm::v4::Location; +use xcm_executor::traits::ConvertLocation; + +use crate::{NewSwitchPairInfoOf, Pallet, SwitchPair, SwitchPairInfoOf}; + +construct_runtime!( + pub enum MockRuntime { + System: frame_system, + Balances: pallet_balances, + Assetswitch: crate + } +); + +impl frame_system::Config for MockRuntime { + type AccountData = AccountData; + type AccountId = AccountId32; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = ConstU64<0>; + type RuntimeTask = (); + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<1>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = ConstU16<0>; + type SystemWeightInfo = (); + type Version = (); +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, MaxEncodedLen, Encode, Decode, Debug, TypeInfo, Default)] +pub struct MockRuntimeHoldReason; + +impl VariantCount for MockRuntimeHoldReason { + const VARIANT_COUNT: u32 = 1; +} + +impl pallet_balances::Config for MockRuntime { + type AccountStore = System; + type Balance = u64; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<2>; + type FreezeIdentifier = [u8; 4]; + type MaxFreezes = ConstU32<1>; + type MaxLocks = ConstU32<0>; + type MaxReserves = ConstU32<0>; + type ReserveIdentifier = (); + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = MockRuntimeHoldReason; + type RuntimeFreezeReason = (); + type WeightInfo = (); +} + +impl crate::Config for MockRuntime { + type AccountIdConverter = (); + type AssetTransactor = (); + type FeeOrigin = EnsureRoot; + type LocalCurrency = Balances; + type PauseOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SubmitterOrigin = EnsureSigned; + type SwitchHooks = (); + type SwitchOrigin = EnsureRoot; + type XcmRouter = (); + type WeightInfo = (); + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +pub(super) const SUCCESSFUL_ACCOUNT_ID: AccountId32 = AccountId32::new([100; 32]); +pub(super) struct SuccessfulAccountIdConverter; + +impl ConvertLocation for SuccessfulAccountIdConverter { + fn convert_location(_location: &Location) -> Option { + Some(SUCCESSFUL_ACCOUNT_ID) + } +} + +pub(super) struct FailingAccountIdConverter; + +impl ConvertLocation for FailingAccountIdConverter { + fn convert_location(_location: &Location) -> Option { + None + } +} + +#[derive(Default)] +pub(super) struct ExtBuilder( + Option>, + Vec<(AccountId32, u64, u64, u64)>, +); + +impl ExtBuilder { + pub(super) fn with_switch_pair_info(mut self, switch_pair_info: NewSwitchPairInfoOf) -> Self { + self.0 = Some(switch_pair_info); + self + } + + pub(super) fn with_additional_balance_entries(mut self, balances: Vec<(AccountId32, u64, u64, u64)>) -> Self { + self.1 = balances; + self + } + + pub(super) fn build(self) -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + System::set_block_number(1); + + let local_ed = >::minimum_balance(); + if let Some(switch_pair_info) = &self.0 { + let switch_pair_info = SwitchPairInfoOf::::from_input_unchecked(switch_pair_info.clone()); + + // Set pool balance to local ED + circulating supply, to maintain + // invariants and make them verifiable. + >::mint_into( + &switch_pair_info.pool_account, + local_ed + u64::checked_from(switch_pair_info.remote_asset_circulating_supply).unwrap(), + ) + .unwrap(); + Pallet::::set_switch_pair_bypass_checks( + switch_pair_info.remote_asset_total_supply, + switch_pair_info.remote_asset_id, + switch_pair_info.remote_asset_circulating_supply, + switch_pair_info.remote_reserve_location, + switch_pair_info.remote_asset_ed, + switch_pair_info.remote_xcm_fee, + switch_pair_info.pool_account, + ); + Pallet::::set_switch_pair_status(switch_pair_info.status).unwrap(); + } + for (account, free, held, frozen) in self.1 { + >::mint_into(&account, free).unwrap(); + >::hold(&MockRuntimeHoldReason {}, &account, held).unwrap(); + >::set_freeze(b"test", &account, frozen).unwrap(); + + // If the specified account is the pool account, remove from the registered + // circulating supply the amount of tokens that have been marked as held or + // frozen, to maintain the invariant. + if Some(account) + == self + .0 + .as_ref() + .map(|new_switch_info| new_switch_info.clone().pool_account) + { + // ED can be frozen, so we only need to considered only the ones that go BEYOND + // the ED. + let freezes_more_than_ed = frozen.saturating_sub(local_ed); + SwitchPair::::mutate(|switch_pair| { + if let Some(switch_pair) = switch_pair.as_mut() { + // Calculate all held tokens as not available in the pool, hence not available + // as circulating supply at destination. + switch_pair.remote_asset_circulating_supply = switch_pair + .remote_asset_circulating_supply + .checked_sub(held as u128) + .unwrap(); + // Calculate frozen tokens beyond the Ed as not available in the pool, hence not + // available as circulating supply at destination. + switch_pair.remote_asset_circulating_supply = switch_pair + .remote_asset_circulating_supply + .checked_sub(freezes_more_than_ed as u128) + .unwrap(); + } + }); + } + } + + System::reset_events() + }); + + ext + } + + pub(super) fn build_and_execute_with_sanity_tests(self, run: impl FnOnce()) { + let mut ext = self.build(); + ext.execute_with(|| { + run(); + crate::try_state::do_try_state::(System::block_number()).unwrap(); + }); + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/transact/mod.rs b/pallets/pallet-asset-switch/src/xcm/transact/mod.rs new file mode 100644 index 000000000..2e2e967ab --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transact/mod.rs @@ -0,0 +1,140 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + ensure, + traits::{fungible::Mutate, tokens::Preservation}, +}; +use sp_std::marker::PhantomData; +use xcm::v4::{Asset, AssetId, Error, Fungibility, Location, Result, XcmContext}; +use xcm_executor::traits::{ConvertLocation, TransactAsset}; + +use crate::{traits::SwitchHooks, Config, Event, LocalCurrencyBalanceOf, Pallet, SwitchPair}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +const LOG_TARGET: &str = "xcm::pallet-asset-switch::SwitchPairRemoteAssetTransactor"; + +/// Type implementing [TransactAsset] that moves from the switch pair pool +/// account, if present, as many local tokens as remote assets received into +/// the specified `Location` if the incoming asset ID matches the remote +/// asset ID as specified in the switch pair and if they are both fungible. +pub struct SwitchPairRemoteAssetTransactor(PhantomData<(AccountIdConverter, T, I)>); + +impl TransactAsset for SwitchPairRemoteAssetTransactor +where + AccountIdConverter: ConvertLocation, + T: Config, + I: 'static, +{ + fn deposit_asset(what: &Asset, who: &Location, context: Option<&XcmContext>) -> Result { + log::info!(target: LOG_TARGET, "deposit_asset {:?} {:?} {:?}", what, who, context); + // 1. Verify the switch pair exists. + let switch_pair = SwitchPair::::get().ok_or(Error::AssetNotFound)?; + + // 2. Verify the asset matches the other side of the switch pair. + let remote_asset_id_v4: AssetId = switch_pair.remote_asset_id.clone().try_into().map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert stored asset ID {:?} into required version with error {:?}.", + switch_pair.remote_asset_id, + e + ); + Error::AssetNotFound + })?; + ensure!(remote_asset_id_v4 == what.id, Error::AssetNotFound); + // 3. Verify the asset being deposited is fungible. + let Fungibility::Fungible(fungible_amount) = what.fun else { + return Err(Error::AssetNotFound); + }; + // After this ensure, we know we need to be transacting with this asset, so any + // errors thrown from here onwards is a `FailedToTransactAsset` error. + + // 4. Verify the switch pair is running. + ensure!( + switch_pair.is_enabled(), + Error::FailedToTransactAsset("switch pair is not running.",) + ); + + let beneficiary = AccountIdConverter::convert_location(who).ok_or(Error::FailedToTransactAsset( + "Failed to convert beneficiary to valid account.", + ))?; + // 5. Call into the pre-switch hook + T::SwitchHooks::pre_remote_to_local_switch(&beneficiary, fungible_amount).map_err(|e| { + log::error!( + target: LOG_TARGET, + "Hook pre-switch check failed with error code {:?}", + e.into() + ); + Error::FailedToTransactAsset("Failed to validate preconditions for remote-to-local switch.") + })?; + + // 6. Perform the local transfer + let fungible_amount_as_currency_balance: LocalCurrencyBalanceOf = + fungible_amount.try_into().map_err(|_| { + Error::FailedToTransactAsset("Failed to convert fungible amount to balance of local currency.") + })?; + T::LocalCurrency::transfer( + &switch_pair.pool_account, + &beneficiary, + fungible_amount_as_currency_balance, + Preservation::Preserve, + ) + .map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to transfer assets from pool account with error {:?}", + e + ); + Error::FailedToTransactAsset("Failed to transfer assets from pool account to specified account.") + })?; + + // 6. Increase the balance of the remote asset + SwitchPair::::try_mutate(|entry| { + let switch_pair_info = entry + .as_mut() + .ok_or(Error::FailedToTransactAsset("SwitchPair should not be None."))?; + switch_pair_info + .try_process_incoming_switch(fungible_amount) + .map_err(|_| { + Error::FailedToTransactAsset("Failed to apply the transfer outcome to the storage components.") + })?; + Ok::<_, Error>(()) + })?; + + // 7. Call into the post-switch hook + T::SwitchHooks::post_remote_to_local_switch(&beneficiary, fungible_amount).map_err(|e| { + log::error!( + target: LOG_TARGET, + "Hook post-switch check failed with error code {:?}", + e.into() + ); + Error::FailedToTransactAsset("Failed to validate postconditions for remote-to-local switch.") + })?; + + Pallet::::deposit_event(Event::::RemoteToLocalSwitchExecuted { + amount: fungible_amount, + to: beneficiary, + }); + + Ok(()) + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/transact/tests/deposit_asset.rs b/pallets/pallet-asset-switch/src/xcm/transact/tests/deposit_asset.rs new file mode 100644 index 000000000..43adc99e7 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transact/tests/deposit_asset.rs @@ -0,0 +1,533 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{assert_noop, assert_ok, traits::fungible::Inspect as InspectFungible}; +use sp_runtime::AccountId32; +use xcm::{ + v4::{Asset, AssetId, AssetInstance, Error, Fungibility, Junction, Junctions, Location}, + IntoVersion, +}; +use xcm_executor::traits::TransactAsset; + +use crate::{ + xcm::{ + test_utils::get_switch_pair_info_for_remote_location_with_pool_usable_balance, + transact::mock::{ + Balances, ExtBuilder, FailingAccountIdConverter, MockRuntime, SuccessfulAccountIdConverter, System, + SUCCESSFUL_ACCOUNT_ID, + }, + SwitchPairRemoteAssetTransactor, + }, + Event, SwitchPairStatus, +}; + +#[test] +fn successful_with_stored_remote_asset_id_latest() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + // Pool account balance = ED (2) + 2 + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 2, SwitchPairStatus::Running); + // Set remote asset to the latest XCM version. + new_switch_pair_info.remote_asset_id = new_switch_pair_info.remote_asset_id.into_latest().unwrap(); + new_switch_pair_info + }; + // Ignored by the mock converter logic + let who = Location::here(); + + // Works if all balance is free + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }; + assert_ok!(SwitchPairRemoteAssetTransactor::< + SuccessfulAccountIdConverter, + MockRuntime, + _, + >::deposit_asset(&asset_to_deposit, &who, None)); + // Reduced by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + // Destination account created and increased by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::RemoteToLocalSwitchExecuted { + amount: 2, + to: SUCCESSFUL_ACCOUNT_ID + } + .into())); + }); + // Works if some balance is frozen, since freezes count towards ED as well. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + // We freeze 2 units for the pool account + .with_additional_balance_entries(vec![(new_switch_pair_info.clone().pool_account, 0, 0, 2)]) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }; + assert_ok!(SwitchPairRemoteAssetTransactor::< + SuccessfulAccountIdConverter, + MockRuntime, + _, + >::deposit_asset(&asset_to_deposit, &who, None)); + // Reduced by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + // Destination account created and increased by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::RemoteToLocalSwitchExecuted { + amount: 2, + to: SUCCESSFUL_ACCOUNT_ID + } + .into())); + }); +} + +#[test] +fn successful_with_stored_remote_asset_id_v4() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location, 2, SwitchPairStatus::Running); + // Set remote asset to the XCM version 3. + new_switch_pair_info.remote_asset_id = new_switch_pair_info.remote_asset_id.into_version(3).unwrap(); + new_switch_pair_info + }; + // Ignored by the mock converter logic + let who = Location::here(); + + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }; + assert_ok!(SwitchPairRemoteAssetTransactor::< + SuccessfulAccountIdConverter, + MockRuntime, + _, + >::deposit_asset(&asset_to_deposit, &who, None)); + // Reduced by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + // Destination account created and increased by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::RemoteToLocalSwitchExecuted { + amount: 2, + to: SUCCESSFUL_ACCOUNT_ID + } + .into())); + }); + // Works if some balance is frozen, since freezes count towards ED as well. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + // We freeze 2 units for the pool account + .with_additional_balance_entries(vec![(new_switch_pair_info.clone().pool_account, 0, 0, 2)]) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }; + assert_ok!(SwitchPairRemoteAssetTransactor::< + SuccessfulAccountIdConverter, + MockRuntime, + _, + >::deposit_asset(&asset_to_deposit, &who, None)); + // Reduced by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + // Destination account created and increased by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::RemoteToLocalSwitchExecuted { + amount: 2, + to: SUCCESSFUL_ACCOUNT_ID + } + .into())); + }); +} + +#[test] +fn successful_with_stored_remote_asset_id_v3() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = + { + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::< + MockRuntime, + >(&location.try_into().unwrap(), 2, SwitchPairStatus::Running); + // Set remote asset to the XCM version 3. + new_switch_pair_info.remote_asset_id = new_switch_pair_info.remote_asset_id.into_version(3).unwrap(); + new_switch_pair_info + }; + // Ignored by the mock converter logic + let who = Location::here(); + + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }; + assert_ok!(SwitchPairRemoteAssetTransactor::< + SuccessfulAccountIdConverter, + MockRuntime, + _, + >::deposit_asset(&asset_to_deposit, &who, None)); + // Reduced by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + // Destination account created and increased by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::RemoteToLocalSwitchExecuted { + amount: 2, + to: SUCCESSFUL_ACCOUNT_ID + } + .into())); + }); + // Works if some balance is frozen, since freezes count towards ED as well. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + // We freeze 2 units for the pool account + .with_additional_balance_entries(vec![(new_switch_pair_info.clone().pool_account, 0, 0, 2)]) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }; + assert_ok!(SwitchPairRemoteAssetTransactor::< + SuccessfulAccountIdConverter, + MockRuntime, + _, + >::deposit_asset(&asset_to_deposit, &who, None)); + // Reduced by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + // Destination account created and increased by 2. + assert_eq!( + >::balance(&new_switch_pair_info.pool_account), + 2 + ); + assert!(System::events().into_iter().map(|e| e.event).any(|e| e + == Event::::RemoteToLocalSwitchExecuted { + amount: 2, + to: SUCCESSFUL_ACCOUNT_ID + } + .into())); + }); +} + +#[test] +fn skips_on_switch_pair_not_set() { + let who = Location::here(); + + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: AssetId(Junctions::Here.into()), + fun: Fungibility::Fungible(2), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::AssetNotFound + ); + }); +} + +#[test] +fn skips_on_different_input_asset_id() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 2, + SwitchPairStatus::Running, + ); + let who = Location::here(); + + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + // Different than what's stored. + id: AssetId(Location::parent()), + fun: Fungibility::Fungible(2), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::AssetNotFound + ); + }); +} + +#[test] +fn skips_on_non_fungible_input_asset() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 2, + SwitchPairStatus::Running, + ); + let who = Location::here(); + + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + // Different than what's stored. + fun: Fungibility::NonFungible(AssetInstance::Index(1)), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::AssetNotFound + ); + }); +} + +#[test] +fn fails_on_switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 2, + SwitchPairStatus::Paused, + ); + let who = Location::here(); + + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::FailedToTransactAsset("switch pair is not running.") + ); + }); +} + +#[test] +fn fails_on_failed_account_id_conversion() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 2, + SwitchPairStatus::Running, + ); + let who = Location::here(); + + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(2), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::FailedToTransactAsset("Failed to convert beneficiary to valid account."), + ); + }); +} + +#[test] +fn fails_on_not_enough_funds_in_pool() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 2, + SwitchPairStatus::Running, + ); + let who = Location::here(); + + // Fails if reducible balance less than requested amount. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + // Amount bigger than the reducible balance of the pool (which is `2`). + fun: Fungibility::Fungible(3), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::FailedToTransactAsset("Failed to transfer assets from pool account to specified account.") + ); + }); + // Fails if balance - holds less than requested amount. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .with_additional_balance_entries(vec![(new_switch_pair_info.clone().pool_account, 0, 1, 0)]) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + // Amount bigger than the reducible balance of the pool (which is `1`, 4 - 2 (ED) - 1 (hold)). + fun: Fungibility::Fungible(2), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::FailedToTransactAsset("Failed to transfer assets from pool account to specified account.") + ); + }); + // Fails if freezes are higher than the requested amount. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + // Freezes do not reduce the reducible balance if they are less than ED. + .with_additional_balance_entries(vec![(new_switch_pair_info.clone().pool_account, 0, 0, 3)]) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + // Amount bigger than the reducible balance of the pool (which is `1`, 4 - 2 (ED) - 1 (freeze beyond + // ED)). + fun: Fungibility::Fungible(2), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::FailedToTransactAsset("Failed to transfer assets from pool account to specified account.") + ); + }); +} + +#[test] +fn fails_on_amount_below_ed() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location_with_pool_usable_balance::( + &location, + 2, + SwitchPairStatus::Running, + ); + let who = Location::here(); + + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build_and_execute_with_sanity_tests(|| { + let asset_to_deposit = Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + // ED is 2, anything that would not result in the account having at least ED will fail. + fun: Fungibility::Fungible(1), + }; + assert_noop!( + SwitchPairRemoteAssetTransactor::::deposit_asset( + &asset_to_deposit, + &who, + None + ), + Error::FailedToTransactAsset("Failed to transfer assets from pool account to specified account."), + ); + }); +} diff --git a/pallets/pallet-asset-switch/src/xcm/transact/tests/mod.rs b/pallets/pallet-asset-switch/src/xcm/transact/tests/mod.rs new file mode 100644 index 000000000..8b20e6067 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transact/tests/mod.rs @@ -0,0 +1,19 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod deposit_asset; diff --git a/pallets/pallet-asset-switch/src/xcm/transfer/mock.rs b/pallets/pallet-asset-switch/src/xcm/transfer/mock.rs new file mode 100644 index 000000000..1bf84a5a8 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transfer/mock.rs @@ -0,0 +1,174 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + construct_runtime, + traits::{ + fungible::{Dust, Inspect as InspectFungible, Mutate as MutateFungible, Unbalanced as UnbalancedFungible}, + tokens::{DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence}, + Everything, + }, +}; +use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSigned}; +use pallet_balances::AccountData; +use sp_core::{ConstU16, ConstU32, ConstU64, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, DispatchError, +}; + +use crate::{NewSwitchPairInfoOf, Pallet}; + +construct_runtime!( + pub enum MockRuntime { + System: frame_system, + Assetswitch: crate + } +); + +impl frame_system::Config for MockRuntime { + type AccountData = AccountData; + type AccountId = AccountId32; + type RuntimeTask = (); + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = ConstU64<0>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<1>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = ConstU16<0>; + type SystemWeightInfo = (); + type Version = (); +} + +// Currency is not used in this XCM component tests, so we mock the entire +// currency system. +pub struct MockCurrency; + +impl MutateFungible for MockCurrency {} + +impl InspectFungible for MockCurrency { + type Balance = u64; + + fn active_issuance() -> Self::Balance { + Self::Balance::default() + } + + fn balance(_who: &AccountId32) -> Self::Balance { + Self::Balance::default() + } + + fn can_deposit(_who: &AccountId32, _amount: Self::Balance, _provenance: Provenance) -> DepositConsequence { + DepositConsequence::Success + } + + fn can_withdraw(_who: &AccountId32, _amount: Self::Balance) -> WithdrawConsequence { + WithdrawConsequence::Success + } + + fn minimum_balance() -> Self::Balance { + Self::Balance::default() + } + + fn reducible_balance(_who: &AccountId32, _preservation: Preservation, _force: Fortitude) -> Self::Balance { + Self::Balance::default() + } + + fn total_balance(_who: &AccountId32) -> Self::Balance { + Self::Balance::default() + } + + fn total_issuance() -> Self::Balance { + Self::Balance::default() + } +} + +impl UnbalancedFungible for MockCurrency { + fn handle_dust(_dust: Dust) {} + + fn write_balance(_who: &AccountId32, _amount: Self::Balance) -> Result, DispatchError> { + Ok(Some(Self::Balance::default())) + } + + fn set_total_issuance(_amount: Self::Balance) {} +} + +impl crate::Config for MockRuntime { + type AccountIdConverter = (); + type AssetTransactor = (); + type FeeOrigin = EnsureRoot; + type LocalCurrency = MockCurrency; + type PauseOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SubmitterOrigin = EnsureSigned; + type SwitchHooks = (); + type SwitchOrigin = EnsureRoot; + type XcmRouter = (); + type WeightInfo = (); + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +#[derive(Default)] +pub(super) struct ExtBuilder(Option>); + +impl ExtBuilder { + pub(super) fn with_switch_pair_info(mut self, switch_pair_info: NewSwitchPairInfoOf) -> Self { + self.0 = Some(switch_pair_info); + self + } + + pub(super) fn build(self) -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + System::set_block_number(1); + + if let Some(switch_pair_info) = self.0 { + Pallet::::set_switch_pair_bypass_checks( + switch_pair_info.remote_asset_total_supply, + switch_pair_info.remote_asset_id, + switch_pair_info.remote_asset_circulating_supply, + switch_pair_info.remote_reserve_location, + switch_pair_info.remote_asset_ed, + switch_pair_info.remote_xcm_fee, + switch_pair_info.pool_account, + ); + Pallet::::set_switch_pair_status(switch_pair_info.status).unwrap(); + } + + System::reset_events() + }); + + ext + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/transfer/mod.rs b/pallets/pallet-asset-switch/src/xcm/transfer/mod.rs new file mode 100644 index 000000000..37b94931e --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transfer/mod.rs @@ -0,0 +1,26 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod xcm_fee_asset; +pub use xcm_fee_asset::IsSwitchPairXcmFeeAsset; + +mod switch_pair_remote_asset; +pub use switch_pair_remote_asset::IsSwitchPairRemoteAsset; + +#[cfg(test)] +mod mock; diff --git a/pallets/pallet-asset-switch/src/xcm/transfer/switch_pair_remote_asset/mod.rs b/pallets/pallet-asset-switch/src/xcm/transfer/switch_pair_remote_asset/mod.rs new file mode 100644 index 000000000..507795926 --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transfer/switch_pair_remote_asset/mod.rs @@ -0,0 +1,74 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::traits::ContainsPair; +use sp_std::marker::PhantomData; +use xcm::v4::{Asset, AssetId, Location}; + +use crate::{Config, SwitchPair}; + +#[cfg(test)] +mod tests; + +const LOG_TARGET: &str = "xcm::barriers::pallet-asset-switch::AllowSwitchPairRemoteAsset"; + +/// Type implementing [ContainsPair] and returns +/// `true` if the specified asset ID matches the switch pair remote asset ID, +/// which must be reserve transferred to this chain to be traded back for +/// the local token. The fungibility of either asset is not checked, and that +/// logic is delegated to the other XCM components, such as the asset +/// transactor(s). +pub struct IsSwitchPairRemoteAsset(PhantomData<(T, I)>); + +impl ContainsPair for IsSwitchPairRemoteAsset +where + T: Config, + I: 'static, +{ + fn contains(a: &Asset, b: &Location) -> bool { + log::info!(target: LOG_TARGET, "contains {:?}, {:?}", a, b); + // 1. Verify a switch pair has been set. We don't care if it's enabled at this + // stage, as we still want the assets to move inside this system. + let Some(switch_pair) = SwitchPair::::get() else { + return false; + }; + + // 2. We only trust the EXACT configured remote location (no parent is allowed). + let Ok(stored_remote_reserve_location_v4): Result = switch_pair.remote_reserve_location.clone().try_into().map_err(|e| { + log::error!(target: LOG_TARGET, "Failed to convert stored remote reserve location {:?} into v4 xcm version with error {:?}.", switch_pair.remote_reserve_location, e); + e + }) else { return false; }; + if stored_remote_reserve_location_v4 != *b { + log::trace!( + target: LOG_TARGET, + "Remote origin {:?} does not match expected origin {:?}", + b, + stored_remote_reserve_location_v4 + ); + return false; + } + + // 3. Verify the asset ID matches the remote asset ID to switch for local ones. + let Ok(stored_remote_asset_id): Result = switch_pair.remote_asset_id.clone().try_into().map_err(|e| { + log::error!(target: LOG_TARGET, "Failed to convert stored remote asset ID {:?} into v4 xcm version with error {:?}.", switch_pair.remote_asset_id, e); + e + }) else { return false; }; + + a.id == stored_remote_asset_id + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/transfer/switch_pair_remote_asset/tests.rs b/pallets/pallet-asset-switch/src/xcm/transfer/switch_pair_remote_asset/tests.rs new file mode 100644 index 000000000..97fcb9a5b --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transfer/switch_pair_remote_asset/tests.rs @@ -0,0 +1,558 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::traits::ContainsPair; +use xcm::{ + v4::{Asset, AssetId, AssetInstance, Fungibility, Junction, Junctions, Location}, + IntoVersion, +}; + +use crate::{ + xcm::{ + test_utils::get_switch_pair_info_for_remote_location, + transfer::mock::{ExtBuilder, MockRuntime}, + IsSwitchPairRemoteAsset, + }, + SwitchPairStatus, +}; + +#[test] +fn true_with_stored_remote_asset_latest() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set remote asset to the latest XCM version. + new_switch_pair_info.remote_asset_id = new_switch_pair_info.remote_asset_id.into_latest().unwrap(); + new_switch_pair_info + }; + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_asset_v4() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_asset_v3() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_location_latest() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_location_v4() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_location_v3() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_location_v2() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set remote location to the XCM v2. + new_switch_pair_info.remote_reserve_location = + new_switch_pair_info.remote_reserve_location.into_version(2).unwrap(); + new_switch_pair_info + }; + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + &new_switch_pair_info + .clone() + .remote_reserve_location + .into_version(3) + .unwrap() + .try_into() + .unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + &new_switch_pair_info + .clone() + .remote_reserve_location + .into_version(3) + .unwrap() + .try_into() + .unwrap() + )); + }); +} + +#[test] +fn false_on_switch_pair_not_set() { + ExtBuilder::default().build().execute_with(|| { + assert!(!IsSwitchPairRemoteAsset::::contains( + &Asset { + id: AssetId(Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()) + }), + fun: Fungibility::Fungible(1) + }, + &Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()) + } + )); + }); +} + +#[test] +fn true_on_switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Paused); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn false_on_different_remote_location() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Fails with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + &Location { + parents: 1, + interior: Junctions::X2([Junction::Parachain(1_000), Junction::PalletInstance(1)].into()) + }, + )); + }); + // Fails with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + // Use a different location that does not match the stored one. + &Location { + parents: 1, + interior: Junctions::X2([Junction::Parachain(1_000), Junction::PalletInstance(1)].into()) + }, + )); + }); +} + +#[test] +fn false_on_nested_remote_location() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + &Location { + parents: 1, + interior: Junctions::X2( + [ + Junction::Parachain(1_000), + Junction::AccountId32 { + network: None, + id: [0; 32] + } + ] + .into() + ) + } + )); + }); +} + +#[test] +fn false_on_parent_remote_location() { + let location = Location { + parents: 1, + interior: Junctions::X2( + [ + Junction::Parachain(1_000), + Junction::AccountId32 { + network: None, + id: [0; 32], + }, + ] + .into(), + ), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairRemoteAsset::::contains( + &Asset { + id: new_switch_pair_info.clone().remote_asset_id.try_into().unwrap(), + fun: Fungibility::Fungible(1) + }, + &Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()) + } + )); + }); +} + +#[test] +fn false_on_different_remote_asset_id() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairRemoteAsset::::contains( + &Asset { + id: AssetId(Location::parent()), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn false_on_nested_remote_asset_id() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairRemoteAsset::::contains( + &Asset { + // Nested location inside configured remote location + id: AssetId(Location { + parents: 1, + interior: Junctions::X2( + [ + Junction::Parachain(1_000), + Junction::AccountId32 { + network: None, + id: [0; 32] + } + ] + .into() + ), + }), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn false_on_parent_remote_asset_id() { + let location = Location { + parents: 1, + interior: Junctions::X2( + [ + Junction::Parachain(1_000), + Junction::AccountId32 { + network: None, + id: [0; 32], + }, + ] + .into(), + ), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairRemoteAsset::::contains( + &Asset { + // Parent location of configured remote location + id: AssetId(Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} diff --git a/pallets/pallet-asset-switch/src/xcm/transfer/xcm_fee_asset/mod.rs b/pallets/pallet-asset-switch/src/xcm/transfer/xcm_fee_asset/mod.rs new file mode 100644 index 000000000..e2c4d198e --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transfer/xcm_fee_asset/mod.rs @@ -0,0 +1,74 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::traits::ContainsPair; +use sp_std::marker::PhantomData; +use xcm::v4::{Asset, Location}; + +use crate::{Config, SwitchPair}; + +#[cfg(test)] +mod tests; + +const LOG_TARGET: &str = "xcm::pallet-asset-switch::AllowXcmFeeAsset"; + +/// Type implementing [ContainsPair] and returns +/// `true` if the specified asset matches the switch pair remote XCM fee +/// asset, which must be reserve transferred to this chain in order to be +/// withdrawn from the user's balance to pay for XCM fees at destination. The +/// fungibility of either asset is not checked, and that logic is delegated to +/// the other XCM components, such as the asset transactor(s). +pub struct IsSwitchPairXcmFeeAsset(PhantomData<(T, I)>); + +impl ContainsPair for IsSwitchPairXcmFeeAsset +where + T: Config, + I: 'static, +{ + fn contains(a: &Asset, b: &Location) -> bool { + log::info!(target: LOG_TARGET, "contains {:?}, {:?}", a, b); + // 1. Verify a switch pair has been set. We don't care if it's enabled at this + // stage, as we still want the assets to move inside this system. + let Some(switch_pair) = SwitchPair::::get() else { + return false; + }; + + // 2. We only trust the EXACT configured remote location (no parent is allowed). + let Ok(stored_remote_reserve_location_v4): Result = switch_pair.remote_reserve_location.clone().try_into().map_err(|e| { + log::error!(target: LOG_TARGET, "Failed to convert stored remote reserve location {:?} into v4 xcm version with error {:?}.", switch_pair.remote_reserve_location, e); + e + }) else { return false; }; + if stored_remote_reserve_location_v4 != *b { + log::trace!( + target: LOG_TARGET, + "Remote origin {:?} does not match expected origin {:?}", + b, + stored_remote_reserve_location_v4 + ); + return false; + } + + // 3. Verify the asset ID matches the configured XCM fee asset ID. + let Ok(stored_remote_asset_fee): Result = switch_pair.remote_xcm_fee.clone().try_into().map_err(|e| { + log::error!(target: LOG_TARGET, "Failed to convert stored remote asset fee {:?} into v4 xcm version with error {:?}.", switch_pair.remote_xcm_fee, e); + e + }) else { return false; }; + + a.id == stored_remote_asset_fee.id + } +} diff --git a/pallets/pallet-asset-switch/src/xcm/transfer/xcm_fee_asset/tests.rs b/pallets/pallet-asset-switch/src/xcm/transfer/xcm_fee_asset/tests.rs new file mode 100644 index 000000000..a803778bd --- /dev/null +++ b/pallets/pallet-asset-switch/src/xcm/transfer/xcm_fee_asset/tests.rs @@ -0,0 +1,592 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::traits::ContainsPair; +use xcm::{ + v4::{Asset, AssetId, AssetInstance, Fungibility, Junction, Junctions, Location}, + IntoVersion, +}; + +use crate::{ + xcm::{ + test_utils::get_switch_pair_info_for_remote_location, + transfer::mock::{ExtBuilder, MockRuntime}, + IsSwitchPairXcmFeeAsset, + }, + SwitchPairStatus, +}; + +#[test] +fn true_with_stored_xcm_fee_asset_latest() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = { + let mut new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Set XCM fee asset to the latest XCM version. + new_switch_pair_info.remote_xcm_fee = new_switch_pair_info.remote_xcm_fee.into_latest().unwrap(); + new_switch_pair_info + }; + // Works with XCM fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with XCM non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_xcm_fee_asset_v4() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_xcm_fee_asset_v3() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_xcm_fee_asset_v2() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_location_latest() { + let location = xcm::latest::Location { + parents: 1, + interior: xcm::latest::Junctions::X1([xcm::latest::Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_location_v4() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_location_v3() { + let location = xcm::v3::MultiLocation { + parents: 1, + interior: xcm::v3::Junctions::X1(xcm::v3::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn true_with_stored_remote_location_v2() { + let location = xcm::v2::MultiLocation { + parents: 1, + interior: xcm::v2::Junctions::X1(xcm::v2::Junction::Parachain(1_000)), + }; + let new_switch_pair_info = { + let location_v3: xcm::v3::MultiLocation = location.try_into().unwrap(); + let mut new_switch_pair_info = get_switch_pair_info_for_remote_location::( + &location_v3.try_into().unwrap(), + SwitchPairStatus::Running, + ); + // Set remote location to the XCM v2. + new_switch_pair_info.remote_reserve_location = + new_switch_pair_info.remote_reserve_location.into_version(2).unwrap(); + new_switch_pair_info + }; + // Works with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + &new_switch_pair_info + .clone() + .remote_reserve_location + .into_version(3) + .unwrap() + .try_into() + .unwrap() + )); + }); + // Works with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + &new_switch_pair_info + .clone() + .remote_reserve_location + .into_version(3) + .unwrap() + .try_into() + .unwrap() + )); + }); +} + +#[test] +fn false_on_switch_pair_not_set() { + ExtBuilder::default().build().execute_with(|| { + assert!(!IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: AssetId(Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()) + }), + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + &Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()) + } + )); + }); +} + +#[test] +fn true_on_switch_pair_not_enabled() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Paused); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.clone().remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn false_on_different_remote_location() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + // Fails with remote fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::Fungible(1) + }, + &Location { + parents: 1, + interior: Junctions::X2([Junction::Parachain(1_000), Junction::PalletInstance(1)].into()) + }, + )); + }); + // Fails with remote non-fungible asset. + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: Asset::try_from(new_switch_pair_info.clone().remote_xcm_fee).unwrap().id, + fun: Fungibility::NonFungible(AssetInstance::Index(1)) + }, + // Use a different location that does not match the stored one. + &Location { + parents: 1, + interior: Junctions::X2([Junction::Parachain(1_000), Junction::PalletInstance(1)].into()) + }, + )); + }); +} + +#[test] +fn false_on_nested_remote_location() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairXcmFeeAsset::::contains( + new_switch_pair_info.clone().remote_xcm_fee.try_as().unwrap(), + &Location { + parents: 1, + interior: Junctions::X2( + [ + Junction::Parachain(1_000), + Junction::AccountId32 { + network: None, + id: [0; 32] + } + ] + .into() + ) + } + )); + }); +} + +#[test] +fn false_on_parent_remote_location() { + let location = Location { + parents: 1, + interior: Junctions::X2( + [ + Junction::Parachain(1_000), + Junction::AccountId32 { + network: None, + id: [0; 32], + }, + ] + .into(), + ), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairXcmFeeAsset::::contains( + new_switch_pair_info.clone().remote_xcm_fee.try_as().unwrap(), + &Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()) + } + )); + }); +} + +#[test] +fn false_on_different_xcm_fee_asset_id() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairXcmFeeAsset::::contains( + &Asset { + id: AssetId(Location::parent()), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn false_on_nested_xcm_fee_asset_id() { + let location = Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairXcmFeeAsset::::contains( + &Asset { + // Nested location inside configured remote location + id: AssetId(Location { + parents: 1, + interior: Junctions::X2( + [ + Junction::Parachain(1_000), + Junction::AccountId32 { + network: None, + id: [0; 32] + } + ] + .into() + ), + }), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} + +#[test] +fn false_on_parent_xcm_fee_asset_id() { + let location = Location { + parents: 1, + interior: Junctions::X2( + [ + Junction::Parachain(1_000), + Junction::AccountId32 { + network: None, + id: [0; 32], + }, + ] + .into(), + ), + }; + let new_switch_pair_info = + get_switch_pair_info_for_remote_location::(&location, SwitchPairStatus::Running); + ExtBuilder::default() + .with_switch_pair_info(new_switch_pair_info.clone()) + .build() + .execute_with(|| { + assert!(!IsSwitchPairXcmFeeAsset::::contains( + &Asset { + // Parent location of configured remote location. + id: AssetId(Location { + parents: 1, + interior: Junctions::X1([Junction::Parachain(1_000)].into()), + }), + fun: Fungibility::Fungible(1) + }, + new_switch_pair_info.remote_reserve_location.try_as().unwrap() + )); + }); +} diff --git a/runtime-api/asset-switch/Cargo.toml b/runtime-api/asset-switch/Cargo.toml new file mode 100644 index 000000000..a1335f787 --- /dev/null +++ b/runtime-api/asset-switch/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = { workspace = true } +description = "Runtime APIs for integrating the asset-switch pallet." +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license-file = { workspace = true } +name = "pallet-asset-switch-runtime-api" +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[dependencies] +# External dependencies +parity-scale-codec = { workspace = true } + +# Substrate dependencies +sp-api = { workspace = true } +sp-std = { workspace = true } + +[features] +default = ["std"] +std = ["parity-scale-codec/std", "sp-api/std", "sp-std/std"] diff --git a/runtime-api/asset-switch/README.md b/runtime-api/asset-switch/README.md new file mode 100644 index 000000000..2771dfdbe --- /dev/null +++ b/runtime-api/asset-switch/README.md @@ -0,0 +1,4 @@ +## Asset switch runtime API + +The asset switch runtime API allows clients to query the following information: +* `fn pool_account_id(pair_id: Vec, asset_id: AssetId) -> AccountId`: the pool address of a to-be-set switch pair given the pallet name in which it will be stored and the switch pair remote asset ID. diff --git a/runtime-api/asset-switch/src/lib.rs b/runtime-api/asset-switch/src/lib.rs new file mode 100644 index 000000000..408c38e3a --- /dev/null +++ b/runtime-api/asset-switch/src/lib.rs @@ -0,0 +1,33 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +#![cfg_attr(not(feature = "std"), no_std)] + +use parity_scale_codec::Codec; +use sp_std::vec::Vec; + +sp_api::decl_runtime_apis! { + /// Runtime API to compute the pool account for a given switch pair ID and remote asset. + pub trait AssetSwitch where + AssetId: Codec, + AccountId: Codec, + Error: Codec, + { + fn pool_account_id(pair_id: Vec, asset_id: AssetId) -> Result; + } +} diff --git a/runtimes/common/Cargo.toml b/runtimes/common/Cargo.toml index 2eca9b1b8..d967da935 100644 --- a/runtimes/common/Cargo.toml +++ b/runtimes/common/Cargo.toml @@ -39,6 +39,7 @@ cumulus-pallet-parachain-system = { workspace = true } cumulus-pallet-xcmp-queue = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } +pallet-assets = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } pallet-membership = { workspace = true } @@ -79,6 +80,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "kilt-dip-primitives/runtime-benchmarks", "kilt-support/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-deposit-storage/runtime-benchmarks", "pallet-membership/runtime-benchmarks", @@ -105,6 +107,7 @@ std = [ "kilt-dip-primitives/std", "kilt-support/std", "log/std", + "pallet-assets/std", "pallet-authorship/std", "pallet-balances/std", "pallet-deposit-storage/std", @@ -141,6 +144,7 @@ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "kilt-support/try-runtime", + "pallet-assets/try-runtime", "pallet-authorship/try-runtime", "pallet-balances/try-runtime", "pallet-membership/try-runtime", diff --git a/runtimes/common/src/asset_switch/mod.rs b/runtimes/common/src/asset_switch/mod.rs new file mode 100644 index 000000000..887075637 --- /dev/null +++ b/runtimes/common/src/asset_switch/mod.rs @@ -0,0 +1,60 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +pub mod runtime_api; + +use frame_support::traits::EnsureOrigin; +use frame_system::EnsureRoot; +use sp_std::marker::PhantomData; + +#[cfg(feature = "runtime-benchmarks")] +pub struct NoopBenchmarkHelper; + +#[cfg(feature = "runtime-benchmarks")] +impl pallet_assets::BenchmarkHelper for NoopBenchmarkHelper { + fn create_asset_id_parameter(_id: u32) -> xcm::v4::Location { + xcm::v4::Location { + parents: 0, + interior: xcm::v4::Junctions::Here, + } + } +} + +/// Returns the `treasury` address if the origin is the root origin. +/// +/// Required by `type CreateOrigin` in `pallet_assets`. +pub struct EnsureRootAsTreasury(PhantomData); + +impl EnsureOrigin for EnsureRootAsTreasury +where + Runtime: pallet_treasury::Config, +{ + type Success = Runtime::AccountId; + + fn try_origin(o: Runtime::RuntimeOrigin) -> Result { + EnsureRoot::try_origin(o)?; + + // Return treasury account ID if successful. + Ok(pallet_treasury::Pallet::::account_id()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + EnsureRoot::try_successful_origin() + } +} diff --git a/runtimes/common/src/asset_switch/runtime_api.rs b/runtimes/common/src/asset_switch/runtime_api.rs new file mode 100644 index 000000000..a7dbaaa78 --- /dev/null +++ b/runtimes/common/src/asset_switch/runtime_api.rs @@ -0,0 +1,27 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; + +#[derive(Debug, Encode, Decode, TypeInfo, PartialEq, Eq, PartialOrd, Ord)] +pub enum Error { + InvalidInput, + SwitchPoolNotFound, + Internal, +} diff --git a/runtimes/common/src/constants.rs b/runtimes/common/src/constants.rs index d6c846ff0..bdf277bc8 100644 --- a/runtimes/common/src/constants.rs +++ b/runtimes/common/src/constants.rs @@ -27,11 +27,11 @@ use parachain_staking::InflationInfo; use crate::{Balance, BlockNumber}; -/// Maximum number of blocks simultaneously accepted by the Runtime, not yet included into the -/// relay chain. +/// Maximum number of blocks simultaneously accepted by the Runtime, not yet +/// included into the relay chain. pub const UNINCLUDED_SEGMENT_CAPACITY: u32 = 1; -/// How many parachain blocks are processed by the relay chain per parent. Limits the number of -/// blocks authored per slot. +/// How many parachain blocks are processed by the relay chain per parent. +/// Limits the number of blocks authored per slot. pub const BLOCK_PROCESSING_VELOCITY: u32 = 1; /// Relay chain slot duration, in milliseconds. pub const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; diff --git a/runtimes/common/src/lib.rs b/runtimes/common/src/lib.rs index ea32d8816..b4b16b6de 100644 --- a/runtimes/common/src/lib.rs +++ b/runtimes/common/src/lib.rs @@ -44,6 +44,7 @@ use sp_runtime::{ }; use sp_std::marker::PhantomData; +pub mod asset_switch; pub mod assets; pub mod authorization; pub mod constants; diff --git a/runtimes/common/src/migrations.rs b/runtimes/common/src/migrations.rs index 412d8e768..19812bf96 100644 --- a/runtimes/common/src/migrations.rs +++ b/runtimes/common/src/migrations.rs @@ -16,23 +16,59 @@ // If you feel like getting in touch with us, you can do so at info@botlabs.org -use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; +use frame_support::{ + pallet_prelude::StorageVersion, + traits::{GetStorageVersion, OnRuntimeUpgrade}, + weights::Weight, +}; use sp_core::Get; use sp_std::marker::PhantomData; +const LOG_TARGET: &str = "migration::BumpStorageVersion"; + /// There are some pallets without a storage version. /// Based on the changes in the PR , /// pallets without a storage version or with a wrong version throw an error /// in the try state tests. pub struct BumpStorageVersion(PhantomData); +const TARGET_PALLET_ASSETS_STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + impl OnRuntimeUpgrade for BumpStorageVersion where - T: frame_system::Config, + T: pallet_assets::Config, { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + if pallet_assets::Pallet::::on_chain_storage_version() < TARGET_PALLET_ASSETS_STORAGE_VERSION { + log::trace!(target: LOG_TARGET, "pallet_assets to be migrated to v1."); + } else { + log::trace!(target: LOG_TARGET, "pallet_assets already on v1. No migration will run."); + } + Ok([].into()) + } + fn on_runtime_upgrade() -> Weight { - log::info!("BumpStorageVersion: Initiating migration"); + log::info!(target: LOG_TARGET, "Initiating migration."); + + if pallet_assets::Pallet::::on_chain_storage_version() < TARGET_PALLET_ASSETS_STORAGE_VERSION { + log::info!(target: LOG_TARGET, "pallet_assets to be migrated to v1."); + TARGET_PALLET_ASSETS_STORAGE_VERSION.put::>(); + ::DbWeight::get().reads_writes(1, 1) + } else { + log::info!(target: LOG_TARGET, "pallet_assets already on v1. No migration will run."); + ::DbWeight::get().reads(1) + } + } - ::DbWeight::get().writes(0) + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: sp_std::vec::Vec) -> Result<(), sp_runtime::TryRuntimeError> { + if pallet_assets::Pallet::::on_chain_storage_version() < TARGET_PALLET_ASSETS_STORAGE_VERSION { + Err(sp_runtime::TryRuntimeError::Other( + "pallet_assets storage version was not updated to v1.", + )) + } else { + Ok(()) + } } } diff --git a/runtimes/common/src/xcm_config.rs b/runtimes/common/src/xcm_config.rs index a1ed20a9f..f0269af99 100644 --- a/runtimes/common/src/xcm_config.rs +++ b/runtimes/common/src/xcm_config.rs @@ -115,8 +115,7 @@ impl ShouldExecute for DenyReserveTransferToRelayChain { Err(ProcessMessageError::Unsupported) // Deny } - // An unexpected reserve transfer has arrived from the Relay Chain. Generally, - // `IsReserve` should not allow this, but we just log it here. + // We don't accept DOTs from the relay chain for the moment. Only AH should pass. ReserveAssetDeposited { .. } if matches!( origin, @@ -126,11 +125,7 @@ impl ShouldExecute for DenyReserveTransferToRelayChain { } ) => { - log::warn!( - target: "xcm::barrier", - "Unexpected ReserveAssetDeposited from the Relay Chain", - ); - Ok(ControlFlow::Continue(())) + Err(ProcessMessageError::Unsupported) // Deny } _ => Ok(ControlFlow::Continue(())), diff --git a/runtimes/peregrine/Cargo.toml b/runtimes/peregrine/Cargo.toml index 446823b1c..66f1f9879 100644 --- a/runtimes/peregrine/Cargo.toml +++ b/runtimes/peregrine/Cargo.toml @@ -14,10 +14,12 @@ version = { workspace = true } substrate-wasm-builder = { workspace = true } [dev-dependencies] -sp-io = { workspace = true } +enum-iterator = { workspace = true } +sp-io = { workspace = true } [dependencies] # External dependencies +cfg-if = { workspace = true } log = { workspace = true } parity-scale-codec = { workspace = true, features = ["derive"] } scale-info = { workspace = true, features = ["derive"] } @@ -29,6 +31,7 @@ frame-system-rpc-runtime-api = { workspace = true } kilt-runtime-api-did = { workspace = true } kilt-runtime-api-public-credentials = { workspace = true } kilt-runtime-api-staking = { workspace = true } +pallet-asset-switch-runtime-api = { workspace = true } pallet-transaction-payment-rpc-runtime-api = { workspace = true } # KILT pallets & primitives @@ -38,6 +41,7 @@ delegation = { workspace = true } did = { workspace = true } kilt-runtime-api-dip-provider = { workspace = true } kilt-support = { workspace = true } +pallet-asset-switch = { workspace = true } pallet-deposit-storage = { workspace = true } pallet-did-lookup = { workspace = true } pallet-dip-provider = { workspace = true } @@ -65,6 +69,7 @@ sp-weights = { workspace = true } frame-executive = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } +pallet-assets = { workspace = true } pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } @@ -134,6 +139,8 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "hex-literal", "kilt-support/runtime-benchmarks", + "pallet-asset-switch/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-democracy/runtime-benchmarks", @@ -192,6 +199,9 @@ std = [ "kilt-runtime-api-staking/std", "kilt-support/std", "log/std", + "pallet-asset-switch-runtime-api/std", + "pallet-asset-switch/std", + "pallet-assets/std", "pallet-aura/std", "pallet-authorship/std", "pallet-balances/std", @@ -259,6 +269,8 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "kilt-support/try-runtime", + "pallet-asset-switch/try-runtime", + "pallet-assets/try-runtime", "pallet-aura/try-runtime", "pallet-authorship/try-runtime", "pallet-balances/try-runtime", diff --git a/runtimes/peregrine/src/asset_switch/mod.rs b/runtimes/peregrine/src/asset_switch/mod.rs new file mode 100644 index 000000000..1e95e5688 --- /dev/null +++ b/runtimes/peregrine/src/asset_switch/mod.rs @@ -0,0 +1,118 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use pallet_asset_switch::traits::SwitchHooks; +use runtime_common::{AccountId, Balance}; +use xcm::{ + v4::{Junction, Junctions, Location}, + VersionedLocation, +}; + +use crate::{KiltToEKiltSwitchPallet, Runtime}; + +const LOG_TARGET: &str = "runtime::peregrine::asset-switch::RestrictTransfersToSameUser"; + +/// Check requiring the beneficiary be a single `AccountId32` junction +/// containing the same account ID as the account on this chain initiating the +/// switch. +pub struct RestrictswitchDestinationToSelf; + +impl SwitchHooks for RestrictswitchDestinationToSelf { + type Error = Error; + + fn pre_local_to_remote_switch( + from: &AccountId, + to: &VersionedLocation, + _amount: Balance, + ) -> Result<(), Self::Error> { + let to_as: Location = to.clone().try_into().map_err(|e| { + log::error!( + target: LOG_TARGET, + "Failed to convert beneficiary Location {:?} to v4 with error {:?}", + to, + e + ); + Error::Internal + })?; + + let junctions: Junctions = [Junction::AccountId32 { + network: None, + id: from.clone().into(), + }] + .into(); + let is_beneficiary_self = to_as.interior == junctions; + cfg_if::cfg_if! { + if #[cfg(feature = "runtime-benchmarks")] { + // Clippy complaints the variable is not used with this feature on, otherwise. + let _ = is_beneficiary_self; + Ok(()) + } else { + frame_support::ensure!(is_beneficiary_self, Error::NotToSelf); + Ok(()) + } + } + } + + // We don't need to take any actions after the switch is executed + fn post_local_to_remote_switch( + _from: &AccountId, + _to: &VersionedLocation, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn pre_remote_to_local_switch(_to: &AccountId, _amount: u128) -> Result<(), Self::Error> { + Ok(()) + } + + fn post_remote_to_local_switch(_to: &AccountId, _amount: u128) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[cfg_attr(test, derive(enum_iterator::Sequence))] +pub enum Error { + NotToSelf, + Internal, +} + +impl From for u8 { + fn from(value: Error) -> Self { + match value { + Error::NotToSelf => 0, + Error::Internal => Self::MAX, + } + } +} + +#[test] +fn error_value_not_duplicated() { + enum_iterator::all::().fold( + sp_std::collections::btree_set::BTreeSet::::new(), + |mut values, new_value| { + let new_encoded_value = u8::from(new_value); + assert!( + values.insert(new_encoded_value), + "Failed to add unique value {:#?} for error variant", + new_encoded_value + ); + values + }, + ); +} diff --git a/runtimes/peregrine/src/lib.rs b/runtimes/peregrine/src/lib.rs index 40df75cde..6a567f1be 100644 --- a/runtimes/peregrine/src/lib.rs +++ b/runtimes/peregrine/src/lib.rs @@ -33,18 +33,16 @@ use frame_support::{ traits::{ fungible::HoldConsideration, tokens::{PayFromAccount, UnityAssetBalanceConversion}, - ConstU32, EitherOfDiverse, EnqueueWithOrigin, Everything, InstanceFilter, LinearStoragePrice, PrivilegeCmp, + AsEnsureOriginWithArg, ConstU32, EitherOfDiverse, EnqueueWithOrigin, Everything, InstanceFilter, + LinearStoragePrice, PrivilegeCmp, }, weights::{ConstantMultiplier, Weight}, }; use frame_system::{pallet_prelude::BlockNumberFor, EnsureRoot, EnsureSigned}; +use pallet_asset_switch::xcm::{AccountId32ToAccountId32JunctionConverter, MatchesSwitchPairXcmFeeFungibleAsset}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; - -#[cfg(feature = "try-runtime")] -use frame_try_runtime::UpgradeCheckSelect; - use sp_api::impl_runtime_apis; -use sp_core::{ConstBool, OpaqueMetadata}; +use sp_core::{ConstBool, ConstU128, OpaqueMetadata}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, ConvertInto, OpaqueKeys}, @@ -53,14 +51,16 @@ use sp_runtime::{ }; use sp_std::{cmp::Ordering, prelude::*}; use sp_version::RuntimeVersion; +use xcm::{v4::Location, VersionedAssetId}; +use xcm_builder::{FungiblesAdapter, NoChecking}; use delegation::DelegationAc; use kilt_support::traits::ItemFilter; use pallet_did_lookup::linkable_account::LinkableAccountId; pub use parachain_staking::InflationInfo; pub use public_credentials; - use runtime_common::{ + asset_switch::{runtime_api::Error as AssetSwitchApiError, EnsureRootAsTreasury}, assets::{AssetDid, PublicCredentialsFilter}, authorization::{AuthorizationId, PalletAuthorize}, constants::{ @@ -76,17 +76,24 @@ use runtime_common::{ Hash, Header, Nonce, SendDustAndFeesToTreasury, Signature, SlowAdjustingFeeUpdate, }; +use crate::xcm_config::{LocationToAccountIdConverter, XcmRouter}; + #[cfg(feature = "std")] use sp_version::NativeVersion; -#[cfg(feature = "runtime-benchmarks")] -use {kilt_support::signature::AlwaysVerify, runtime_common::benchmarks::DummySignature}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; +#[cfg(feature = "runtime-benchmarks")] +use {kilt_support::signature::AlwaysVerify, runtime_common::benchmarks::DummySignature}; + +#[cfg(feature = "try-runtime")] +use frame_try_runtime::UpgradeCheckSelect; + #[cfg(test)] mod tests; +mod asset_switch; mod dip; mod weights; pub mod xcm_config; @@ -969,6 +976,61 @@ impl pallet_proxy::Config for Runtime { type WeightInfo = weights::pallet_proxy::WeightInfo; } +parameter_types! { + pub CheckingAccount: AccountId = PolkadotXcm::check_account(); +} + +pub type KiltToEKiltSwitchPallet = pallet_asset_switch::Instance1; +impl pallet_asset_switch::Config for Runtime { + type AccountIdConverter = AccountId32ToAccountId32JunctionConverter; + type AssetTransactor = FungiblesAdapter< + Fungibles, + MatchesSwitchPairXcmFeeFungibleAsset, + LocationToAccountIdConverter, + AccountId, + NoChecking, + CheckingAccount, + >; + type FeeOrigin = EnsureRoot; + type LocalCurrency = Balances; + type PauseOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SubmitterOrigin = EnsureSigned; + type SwitchHooks = asset_switch::RestrictswitchDestinationToSelf; + type SwitchOrigin = EnsureRoot; + type WeightInfo = weights::pallet_asset_switch::WeightInfo; + type XcmRouter = XcmRouter; + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = crate::benches::CreateFungibleForAssetSwitchPool1; +} + +// No deposit is taken since creation is permissioned. Only the root origin can +// create new assets, and the owner will be the treasury account. +impl pallet_assets::Config for Runtime { + type ApprovalDeposit = ConstU128<0>; + type AssetAccountDeposit = ConstU128<0>; + type AssetDeposit = ConstU128<0>; + type AssetId = Location; + type AssetIdParameter = Location; + type Balance = Balance; + type CallbackHandle = (); + type CreateOrigin = AsEnsureOriginWithArg>; + type Currency = Balances; + type Extra = (); + type ForceOrigin = EnsureRoot; + type Freezer = (); + type MetadataDepositBase = ConstU128<0>; + type MetadataDepositPerByte = ConstU128<0>; + type RemoveItemsLimit = ConstU32<1_000>; + type RuntimeEvent = RuntimeEvent; + type StringLimit = ConstU32<4>; + type WeightInfo = weights::pallet_assets::WeightInfo; + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = runtime_common::asset_switch::NoopBenchmarkHelper; +} + construct_runtime! { pub enum Runtime { @@ -1020,6 +1082,9 @@ construct_runtime! { Multisig: pallet_multisig = 47, + AssetSwitchPool1: pallet_asset_switch:: = 48, + Fungibles: pallet_assets = 49, + // KILT Pallets. Start indices 60 to leave room // DELETED: KiltLaunch: kilt_launch = 60, Ctype: ctype = 61, @@ -1137,6 +1202,7 @@ pub type Executive = frame_executive::Executive< // Executes pallet hooks in the order of definition in construct_runtime AllPalletsWithSystem, ( + runtime_common::migrations::BumpStorageVersion, cumulus_pallet_xcmp_queue::migration::v4::MigrationToV4, pallet_xcm::migration::MigrateToLatestXcmVersion, ), @@ -1145,6 +1211,13 @@ pub type Executive = frame_executive::Executive< #[cfg(feature = "runtime-benchmarks")] mod benches { + use frame_system::RawOrigin; + use pallet_asset_switch::PartialBenchmarkInfo; + use runtime_common::AccountId; + use xcm::v4::{Asset, AssetId, Fungibility, Junction, Junctions, Location, ParentThen}; + + use crate::{Fungibles, ParachainSystem}; + frame_support::parameter_types! { pub const MaxBalance: crate::Balance = crate::Balance::max_value(); } @@ -1181,11 +1254,58 @@ mod benches { [pallet_migration, Migration] [pallet_dip_provider, DipProvider] [pallet_deposit_storage, DepositStorage] + [pallet_asset_switch, AssetSwitchPool1] + [pallet_assets, Fungibles] [pallet_message_queue, MessageQueue] [cumulus_pallet_parachain_system, ParachainSystem] [cumulus_pallet_dmp_queue, DmpQueue] [frame_benchmarking::baseline, Baseline::] ); + + // Required since the pallet `AssetTransactor` will try to deduct the XCM fee + // from the user's balance, and the asset must exist. + pub struct CreateFungibleForAssetSwitchPool1; + + impl pallet_asset_switch::BenchmarkHelper for CreateFungibleForAssetSwitchPool1 { + fn setup() -> Option { + const DESTINATION_PARA_ID: u32 = 1_000; + + let asset_location: Location = Junctions::Here.into(); + Fungibles::create( + RawOrigin::Root.into(), + asset_location.clone(), + AccountId::from([0; 32]).into(), + 1u32.into(), + ) + .unwrap(); + let beneficiary = Junctions::X1( + [Junction::AccountId32 { + network: None, + id: [0; 32], + }] + .into(), + ) + .into(); + let destination = Location::from(ParentThen(Junctions::X1( + [Junction::Parachain(DESTINATION_PARA_ID)].into(), + ))) + .into(); + let remote_xcm_fee = Asset { + id: AssetId(asset_location), + fun: Fungibility::Fungible(1_000), + } + .into(); + + ParachainSystem::open_outbound_hrmp_channel_for_benchmarks_or_tests(DESTINATION_PARA_ID.into()); + + Some(PartialBenchmarkInfo { + beneficiary: Some(beneficiary), + destination: Some(destination), + remote_asset_id: None, + remote_xcm_fee: Some(remote_xcm_fee), + }) + } + } } impl_runtime_apis! { @@ -1469,6 +1589,26 @@ impl_runtime_apis! { } } + impl pallet_asset_switch_runtime_api::AssetSwitch for Runtime { + fn pool_account_id(pair_id: Vec, asset_id: VersionedAssetId) -> Result { + use core::str; + use frame_support::traits::PalletInfoAccess; + + let Ok(pair_id_as_string) = str::from_utf8(pair_id.as_slice()) else { + return Err(AssetSwitchApiError::InvalidInput); + }; + match pair_id_as_string { + kilt_to_ekilt if kilt_to_ekilt == AssetSwitchPool1::name() => { + AssetSwitchPool1::pool_account_id_for_remote_asset(&asset_id).map_err(|e| { + log::error!("Failed to calculate pool account address for asset ID {:?} with error: {:?}", asset_id, e); + AssetSwitchApiError::Internal + }) + }, + _ => Err(AssetSwitchApiError::SwitchPoolNotFound) + } + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> ( @@ -1518,6 +1658,34 @@ impl_runtime_apis! { )) } + fn set_up_complex_asset_transfer() -> Option<(Assets, u32, Location, Box)> { + let (transferable_asset, dest) = Self::reserve_transferable_asset_and_dest().unwrap(); + + let fee_amount = ExistentialDeposit::get(); + let fee_asset: Asset = (Location::here(), fee_amount).into(); + + // Make account free to pay the fee + let who = frame_benchmarking::whitelisted_caller(); + let balance = fee_amount + ExistentialDeposit::get() * 1000; + let _ = >::make_free_balance_be( + &who, balance, + ); + + // verify initial balance + assert_eq!(Balances::free_balance(&who), balance); + + + let assets: Assets = vec![fee_asset.clone(), transferable_asset.clone()].into(); + let fee_index = if assets.get(0).unwrap().eq(&fee_asset) { 0 } else { 1 }; + + let verify = Box::new( move || { + let Fungibility::Fungible(transferable_amount) = transferable_asset.fun else { return; }; + assert_eq!(Balances::free_balance(&who), balance - fee_amount - transferable_amount); + }); + + Some((assets,fee_index , dest, verify)) + } + fn get_asset() -> Asset { xcm_benchmarking::NativeAsset::get() } diff --git a/runtimes/peregrine/src/tests.rs b/runtimes/peregrine/src/tests.rs index 2a416ceba..be87de587 100644 --- a/runtimes/peregrine/src/tests.rs +++ b/runtimes/peregrine/src/tests.rs @@ -41,15 +41,18 @@ use runtime_common::{ use super::{Runtime, RuntimeCall}; -#[test] -fn call_size() { - assert!( - core::mem::size_of::() <= 240, - "size of Call is more than 240 bytes: some calls have too big arguments, use Box to reduce \ - the size of Call. - If the limit is too strong, maybe consider increase the limit to 300.", - ); -} +// TODO: Uncomment if pallet_assets implements measures to reduce their `Call` +// space footprint. + +// #[test] +// fn call_size() { +// assert!( +// core::mem::size_of::() <= 240, +// "size of Call is more than 240 bytes: some calls have too big arguments, use +// Box to reduce \ the size of Call. +// If the limit is too strong, maybe consider increase the limit to 300.", +// ); +// } #[test] fn attestation_storage_sizes() { diff --git a/runtimes/peregrine/src/weights/mod.rs b/runtimes/peregrine/src/weights/mod.rs index 6aaef3eb5..b6ae7a30c 100644 --- a/runtimes/peregrine/src/weights/mod.rs +++ b/runtimes/peregrine/src/weights/mod.rs @@ -23,6 +23,8 @@ pub mod cumulus_pallet_parachain_system; pub mod delegation; pub mod did; pub mod frame_system; +pub mod pallet_asset_switch; +pub mod pallet_assets; pub mod pallet_balances; pub mod pallet_collective; pub mod pallet_democracy; diff --git a/runtimes/peregrine/src/weights/pallet_asset_switch.rs b/runtimes/peregrine/src/weights/pallet_asset_switch.rs new file mode 100644 index 000000000..941a33cfd --- /dev/null +++ b/runtimes/peregrine/src/weights/pallet_asset_switch.rs @@ -0,0 +1,235 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +//! Autogenerated weights for `pallet_asset_switch` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 27.0.0 +//! DATE: 2024-07-19, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `rust-2`, CPU: `12th Gen Intel(R) Core(TM) i9-12900K` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/debug/kilt-parachain +// benchmark +// pallet +// --template=.maintain/runtime-weight-template.hbs +// --header=HEADER-GPL +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --steps=50 +// --repeat=20 +// --chain +// dev +// --pallet +// pallet-asset-switch +// --extrinsic=* +// --output +// ./runtimes/peregrine/src/weights/pallet_asset_switch.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for `pallet_asset_switch`. +pub struct WeightInfo(PhantomData); +impl pallet_asset_switch::WeightInfo for WeightInfo { + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: Some(1891), added: 2386, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + fn set_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `128` + // Estimated: `3597` + // Minimum execution time: 195_975_000 picoseconds. + Weight::from_parts(197_609_000, 0) + .saturating_add(Weight::from_parts(0, 3597)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:0 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: Some(1891), added: 2386, mode: `MaxEncodedLen`) + fn force_set_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 124_504_000 picoseconds. + Weight::from_parts(124_996_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: Some(1891), added: 2386, mode: `MaxEncodedLen`) + fn force_unset_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `3376` + // Minimum execution time: 146_871_000 picoseconds. + Weight::from_parts(149_775_000, 0) + .saturating_add(Weight::from_parts(0, 3376)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: Some(1891), added: 2386, mode: `MaxEncodedLen`) + fn pause_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `3376` + // Minimum execution time: 101_743_000 picoseconds. + Weight::from_parts(102_740_000, 0) + .saturating_add(Weight::from_parts(0, 3376)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: Some(1891), added: 2386, mode: `MaxEncodedLen`) + fn resume_switch_pair() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `3376` + // Minimum execution time: 149_329_000 picoseconds. + Weight::from_parts(154_394_000, 0) + .saturating_add(Weight::from_parts(0, 3376)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: Some(1891), added: 2386, mode: `MaxEncodedLen`) + fn update_remote_xcm_fee() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `3376` + // Minimum execution time: 103_161_000 picoseconds. + Weight::from_parts(105_989_000, 0) + .saturating_add(Weight::from_parts(0, 3376)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:1) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: Some(1891), added: 2386, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0) + /// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1) + /// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0) + /// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::RelevantMessagingState` (r:1 w:0) + /// Proof: `ParachainSystem::RelevantMessagingState` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:1) + /// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `XcmpQueue::OutboundXcmpMessages` (r:0 w:1) + /// Proof: `XcmpQueue::OutboundXcmpMessages` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn switch() -> Weight { + // Proof Size summary in bytes: + // Measured: `1045` + // Estimated: `6204` + // Minimum execution time: 1_435_326_000 picoseconds. + Weight::from_parts(1_466_377_000, 0) + .saturating_add(Weight::from_parts(0, 6204)) + .saturating_add(T::DbWeight::get().reads(10)) + .saturating_add(T::DbWeight::get().writes(8)) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_set_switch_pair() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 3597 + ); + } + #[test] + fn test_force_unset_switch_pair() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 3376 + ); + } + #[test] + fn test_pause_switch_pair() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 3376 + ); + } + #[test] + fn test_resume_switch_pair() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 3376 + ); + } + #[test] + fn update_remote_xcm_fee() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 3376 + ); + } + #[test] + fn test_switch() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 6204 + ); + } +} diff --git a/runtimes/peregrine/src/weights/pallet_assets.rs b/runtimes/peregrine/src/weights/pallet_assets.rs new file mode 100644 index 000000000..caa46f97c --- /dev/null +++ b/runtimes/peregrine/src/weights/pallet_assets.rs @@ -0,0 +1,910 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain 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. + +// The KILT Blockchain 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 this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +//! Autogenerated weights for `pallet_assets` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 33.0.0 +//! DATE: 2024-08-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `rust-2`, CPU: `12th Gen Intel(R) Core(TM) i9-12900K` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release-unoptimized/kilt-parachain +// benchmark +// pallet +// --template=.maintain/runtime-weight-template.hbs +// --header=HEADER-GPL +// --wasm-execution=compiled +// --heap-pages=4096 +// --steps=50 +// --repeat=20 +// --chain +// dev +// --pallet +// pallet-assets +// --extrinsic=* +// --output=./runtimes/peregrine/src/weights/pallet_assets.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for `pallet_assets`. +pub struct WeightInfo(PhantomData); +impl pallet_assets::WeightInfo for WeightInfo { + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `4273` + // Minimum execution time: 7_511_000 picoseconds. + Weight::from_parts(8_022_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn force_create() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `4273` + // Minimum execution time: 7_389_000 picoseconds. + Weight::from_parts(7_993_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn start_destroy() -> Weight { + // Proof Size summary in bytes: + // Measured: `312` + // Estimated: `4273` + // Minimum execution time: 7_724_000 picoseconds. + Weight::from_parts(8_024_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1001 w:1000) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1000 w:1000) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 1000]`. + fn destroy_accounts(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + c * (212 ±0)` + // Estimated: `4273 + c * (3207 ±0)` + // Minimum execution time: 10_650_000 picoseconds. + Weight::from_parts(11_066_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + // Standard Error: 6_907 + .saturating_add(Weight::from_parts(9_817_244, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 3207).saturating_mul(c.into())) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Approvals` (r:1001 w:1000) + /// Proof: `Fungibles::Approvals` (`max_values`: None, `max_size`: Some(746), added: 3221, mode: `MaxEncodedLen`) + /// The range of component `a` is `[0, 1000]`. + fn destroy_approvals(a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `447 + a * (86 ±0)` + // Estimated: `4273 + a * (3221 ±0)` + // Minimum execution time: 10_767_000 picoseconds. + Weight::from_parts(10_982_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + // Standard Error: 3_058 + .saturating_add(Weight::from_parts(3_935_847, 0).saturating_mul(a.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 3221).saturating_mul(a.into())) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Metadata` (r:1 w:0) + /// Proof: `Fungibles::Metadata` (`max_values`: None, `max_size`: Some(646), added: 3121, mode: `MaxEncodedLen`) + fn finish_destroy() -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 9_031_000 picoseconds. + Weight::from_parts(9_270_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + fn mint() -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 16_960_000 picoseconds. + Weight::from_parts(17_381_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + fn burn() -> Weight { + // Proof Size summary in bytes: + // Measured: `384` + // Estimated: `4273` + // Minimum execution time: 22_651_000 picoseconds. + Weight::from_parts(23_338_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:2 w:2) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + fn transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `436` + // Estimated: `7404` + // Minimum execution time: 33_107_000 picoseconds. + Weight::from_parts(33_858_000, 0) + .saturating_add(Weight::from_parts(0, 7404)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:2 w:2) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + fn transfer_keep_alive() -> Weight { + // Proof Size summary in bytes: + // Measured: `436` + // Estimated: `7404` + // Minimum execution time: 29_877_000 picoseconds. + Weight::from_parts(30_769_000, 0) + .saturating_add(Weight::from_parts(0, 7404)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:2 w:2) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + fn force_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `436` + // Estimated: `7404` + // Minimum execution time: 32_722_000 picoseconds. + Weight::from_parts(33_735_000, 0) + .saturating_add(Weight::from_parts(0, 7404)) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `Fungibles::Asset` (r:1 w:0) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + fn freeze() -> Weight { + // Proof Size summary in bytes: + // Measured: `384` + // Estimated: `4273` + // Minimum execution time: 10_858_000 picoseconds. + Weight::from_parts(11_325_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:0) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + fn thaw() -> Weight { + // Proof Size summary in bytes: + // Measured: `384` + // Estimated: `4273` + // Minimum execution time: 10_834_000 picoseconds. + Weight::from_parts(11_284_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn freeze_asset() -> Weight { + // Proof Size summary in bytes: + // Measured: `312` + // Estimated: `4273` + // Minimum execution time: 7_493_000 picoseconds. + Weight::from_parts(7_911_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn thaw_asset() -> Weight { + // Proof Size summary in bytes: + // Measured: `312` + // Estimated: `4273` + // Minimum execution time: 7_613_000 picoseconds. + Weight::from_parts(7_951_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Metadata` (r:1 w:0) + /// Proof: `Fungibles::Metadata` (`max_values`: None, `max_size`: Some(646), added: 3121, mode: `MaxEncodedLen`) + fn transfer_ownership() -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 9_365_000 picoseconds. + Weight::from_parts(9_594_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn set_team() -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 7_928_000 picoseconds. + Weight::from_parts(8_388_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:0) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Metadata` (r:1 w:1) + /// Proof: `Fungibles::Metadata` (`max_values`: None, `max_size`: Some(646), added: 3121, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 4]`. + /// The range of component `s` is `[0, 4]`. + fn set_metadata(n: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 9_177_000 picoseconds. + Weight::from_parts(9_513_737, 0) + .saturating_add(Weight::from_parts(0, 4273)) + // Standard Error: 4_348 + .saturating_add(Weight::from_parts(92_631, 0).saturating_mul(n.into())) + // Standard Error: 4_348 + .saturating_add(Weight::from_parts(76_577, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:0) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Metadata` (r:1 w:1) + /// Proof: `Fungibles::Metadata` (`max_values`: None, `max_size`: Some(646), added: 3121, mode: `MaxEncodedLen`) + fn clear_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `346` + // Estimated: `4273` + // Minimum execution time: 9_824_000 picoseconds. + Weight::from_parts(10_333_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:0) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Metadata` (r:1 w:1) + /// Proof: `Fungibles::Metadata` (`max_values`: None, `max_size`: Some(646), added: 3121, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 4]`. + /// The range of component `s` is `[0, 4]`. + fn force_set_metadata(n: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `117` + // Estimated: `4273` + // Minimum execution time: 8_095_000 picoseconds. + Weight::from_parts(8_730_911, 0) + .saturating_add(Weight::from_parts(0, 4273)) + // Standard Error: 4_722 + .saturating_add(Weight::from_parts(39_054, 0).saturating_mul(n.into())) + // Standard Error: 4_722 + .saturating_add(Weight::from_parts(70_013, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:0) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Metadata` (r:1 w:1) + /// Proof: `Fungibles::Metadata` (`max_values`: None, `max_size`: Some(646), added: 3121, mode: `MaxEncodedLen`) + fn force_clear_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `346` + // Estimated: `4273` + // Minimum execution time: 9_583_000 picoseconds. + Weight::from_parts(9_811_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn force_asset_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 7_572_000 picoseconds. + Weight::from_parts(7_975_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Approvals` (r:1 w:1) + /// Proof: `Fungibles::Approvals` (`max_values`: None, `max_size`: Some(746), added: 3221, mode: `MaxEncodedLen`) + fn approve_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `312` + // Estimated: `4273` + // Minimum execution time: 12_806_000 picoseconds. + Weight::from_parts(13_339_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Approvals` (r:1 w:1) + /// Proof: `Fungibles::Approvals` (`max_values`: None, `max_size`: Some(746), added: 3221, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:2 w:2) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + fn transfer_approved() -> Weight { + // Proof Size summary in bytes: + // Measured: `604` + // Estimated: `7404` + // Minimum execution time: 41_589_000 picoseconds. + Weight::from_parts(42_096_000, 0) + .saturating_add(Weight::from_parts(0, 7404)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(5)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Approvals` (r:1 w:1) + /// Proof: `Fungibles::Approvals` (`max_values`: None, `max_size`: Some(746), added: 3221, mode: `MaxEncodedLen`) + fn cancel_approval() -> Weight { + // Proof Size summary in bytes: + // Measured: `480` + // Estimated: `4273` + // Minimum execution time: 14_768_000 picoseconds. + Weight::from_parts(15_253_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Approvals` (r:1 w:1) + /// Proof: `Fungibles::Approvals` (`max_values`: None, `max_size`: Some(746), added: 3221, mode: `MaxEncodedLen`) + fn force_cancel_approval() -> Weight { + // Proof Size summary in bytes: + // Measured: `480` + // Estimated: `4273` + // Minimum execution time: 14_912_000 picoseconds. + Weight::from_parts(15_381_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn set_min_balance() -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 8_518_000 picoseconds. + Weight::from_parts(8_741_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn touch() -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 13_139_000 picoseconds. + Weight::from_parts(13_604_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn touch_other() -> Weight { + // Proof Size summary in bytes: + // Measured: `278` + // Estimated: `4273` + // Minimum execution time: 12_159_000 picoseconds. + Weight::from_parts(12_811_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn refund() -> Weight { + // Proof Size summary in bytes: + // Measured: `402` + // Estimated: `4273` + // Minimum execution time: 11_554_000 picoseconds. + Weight::from_parts(11_897_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Asset` (r:1 w:1) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + fn refund_other() -> Weight { + // Proof Size summary in bytes: + // Measured: `435` + // Estimated: `4273` + // Minimum execution time: 11_645_000 picoseconds. + Weight::from_parts(11_851_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Fungibles::Asset` (r:1 w:0) + /// Proof: `Fungibles::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) + /// Storage: `Fungibles::Account` (r:1 w:1) + /// Proof: `Fungibles::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + fn block() -> Weight { + // Proof Size summary in bytes: + // Measured: `384` + // Estimated: `4273` + // Minimum execution time: 10_550_000 picoseconds. + Weight::from_parts(10_923_000, 0) + .saturating_add(Weight::from_parts(0, 4273)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_create() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_force_create() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_start_destroy() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_destroy_accounts() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_destroy_approvals() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_finish_destroy() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_mint() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_burn() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_transfer() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 7404 + ); + } + #[test] + fn test_transfer_keep_alive() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 7404 + ); + } + #[test] + fn test_force_transfer() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 7404 + ); + } + #[test] + fn test_freeze() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_thaw() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_freeze_asset() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_thaw_asset() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_transfer_ownership() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_set_team() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_set_metadata() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_clear_metadata() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_force_set_metadata() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_force_clear_metadata() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_force_asset_status() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_approve_transfer() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_transfer_approved() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 7404 + ); + } + #[test] + fn test_cancel_approval() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_force_cancel_approval() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_set_min_balance() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_touch() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_touch_other() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_refund() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_refund_other() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } + #[test] + fn test_block() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 4273 + ); + } +} diff --git a/runtimes/peregrine/src/weights/pallet_xcm.rs b/runtimes/peregrine/src/weights/pallet_xcm.rs index 49d39abdd..68a08614d 100644 --- a/runtimes/peregrine/src/weights/pallet_xcm.rs +++ b/runtimes/peregrine/src/weights/pallet_xcm.rs @@ -107,15 +107,33 @@ impl pallet_xcm::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(4)) } - /// Storage: `Benchmark::Override` (r:0 w:0) - /// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `AssetSwitchPool1::SwitchPair` (r:1 w:0) + /// Proof: `AssetSwitchPool1::SwitchPair` (`max_values`: Some(1), `max_size`: Some(1939), added: 2434, mode: `MaxEncodedLen`) + /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) + /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `PolkadotXcm::SupportedVersion` (r:1 w:0) + /// Proof: `PolkadotXcm::SupportedVersion` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::VersionDiscoveryQueue` (r:1 w:1) + /// Proof: `PolkadotXcm::VersionDiscoveryQueue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::SafeXcmVersion` (r:1 w:0) + /// Proof: `PolkadotXcm::SafeXcmVersion` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::RelevantMessagingState` (r:1 w:0) + /// Proof: `ParachainSystem::RelevantMessagingState` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `XcmpQueue::OutboundXcmpStatus` (r:1 w:1) + /// Proof: `XcmpQueue::OutboundXcmpStatus` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `XcmpQueue::OutboundXcmpMessages` (r:0 w:1) + /// Proof: `XcmpQueue::OutboundXcmpMessages` (`max_values`: None, `max_size`: None, mode: `Measured`) fn transfer_assets() -> Weight { // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. - Weight::from_parts(18_446_744_073_709_551_000, 0) - .saturating_add(Weight::from_parts(0, 0)) + // Measured: `346` + // Estimated: `3811` + // Minimum execution time: 1_048_341_000 picoseconds. + Weight::from_parts(1_058_740_000, 0) + .saturating_add(Weight::from_parts(0, 3811)) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `Benchmark::Override` (r:0 w:0) /// Proof: `Benchmark::Override` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -387,6 +405,18 @@ mod tests { ); } #[test] + fn test_transfer_assets() { + assert!( + ::BlockWeights::get() + .per_class + .get(frame_support::dispatch::DispatchClass::Normal) + .max_extrinsic + .unwrap_or_else(::max_value) + .proof_size() + > 3811 + ); + } + #[test] fn test_force_subscribe_version_notify() { assert!( ::BlockWeights::get() diff --git a/runtimes/peregrine/src/xcm_config.rs b/runtimes/peregrine/src/xcm_config.rs index cd075e3fc..28b1ce3d9 100644 --- a/runtimes/peregrine/src/xcm_config.rs +++ b/runtimes/peregrine/src/xcm_config.rs @@ -17,8 +17,9 @@ // If you feel like getting in touch with us, you can do so at info@botlabs.org use crate::{ - AccountId, AllPalletsWithSystem, Balances, MessageQueue, ParachainInfo, ParachainSystem, PolkadotXcm, Runtime, - RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, XcmpQueue, + AccountId, AllPalletsWithSystem, Balances, CheckingAccount, Fungibles, KiltToEKiltSwitchPallet, MessageQueue, + ParachainInfo, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Treasury, + WeightToFee, XcmpQueue, }; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; @@ -27,6 +28,10 @@ use frame_support::{ traits::{Contains, EnqueueWithOrigin, Everything, Nothing, TransformOrigin}, }; use frame_system::EnsureRoot; +use pallet_asset_switch::xcm::{ + IsSwitchPairRemoteAsset, IsSwitchPairXcmFeeAsset, MatchesSwitchPairXcmFeeFungibleAsset, + SwitchPairRemoteAssetTransactor, UsingComponentsForSwitchPairRemoteAsset, UsingComponentsForXcmFeeAsset, +}; use pallet_xcm::XcmPassthrough; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; use polkadot_runtime_common::xcm_sender::NoPriceForMessageDelivery; @@ -35,9 +40,9 @@ use sp_std::prelude::ToOwned; use xcm::v4::prelude::*; use xcm_builder::{ AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, - EnsureXcmOrigin, FixedWeightBounds, FrameTransactionalProcessor, NativeAsset, RelayChainAsNative, - SiblingParachainAsNative, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, - TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, + EnsureXcmOrigin, FixedWeightBounds, FrameTransactionalProcessor, FungiblesAdapter, NativeAsset, NoChecking, + RelayChainAsNative, SiblingParachainAsNative, SignedAccountId32AsNative, SignedToAccountId32, + SovereignSignedViaLocation, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, }; use xcm_executor::{traits::WithOriginFilter, XcmExecutor}; @@ -167,15 +172,41 @@ impl Contains for SafeCallFilter { } } +parameter_types! { + pub TreasuryAccountId: AccountId = Treasury::account_id(); +} + pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { type RuntimeCall = RuntimeCall; // How we send Xcm messages. type XcmSender = XcmRouter; // How to withdraw and deposit an asset. - type AssetTransactor = LocalAssetTransactor; + // Until fixed, `LocalAssetTransactor` must be last since it returns an error if + // the operation does not go through, i.e., it cannot be chained with other + // transactors. + type AssetTransactor = ( + // Allow the asset from the other side of the pool to be "deposited" into the current system. + SwitchPairRemoteAssetTransactor, + // Allow the asset to pay for remote XCM fees to be deposited into the current system. + FungiblesAdapter< + Fungibles, + MatchesSwitchPairXcmFeeFungibleAsset, + LocationToAccountIdConverter, + AccountId, + NoChecking, + CheckingAccount, + >, + // Transactor for fungibles matching the "Here" location. + LocalAssetTransactor, + ); type OriginConverter = XcmOriginToTransactDispatchOrigin; - type IsReserve = NativeAsset; + type IsReserve = ( + NativeAsset, + IsSwitchPairRemoteAsset, + IsSwitchPairXcmFeeAsset, + ); + // Teleporting is disabled. type IsTeleporter = (); type UniversalLocation = UniversalLocation; @@ -187,8 +218,21 @@ impl xcm_executor::Config for XcmConfig { // How weight is transformed into fees. The fees are not taken out of the // Balances pallet here. Balances is only used if fees are dropped without being // used. In that case they are put into the treasury. - type Trader = - UsingComponents, HereLocation, AccountId, Balances, SendDustAndFeesToTreasury>; + + type Trader = ( + // Can pay for fees with the remote XCM asset fee (when sending it into this system). + UsingComponentsForXcmFeeAsset>, + // Can pay for the remote asset of the switch pair (when "depositing" it into this system). + UsingComponentsForSwitchPairRemoteAsset< + Runtime, + KiltToEKiltSwitchPallet, + WeightToFee, + TreasuryAccountId, + >, + // Can pay with the fungible that matches the "Here" location. + UsingComponents, HereLocation, AccountId, Balances, SendDustAndFeesToTreasury>, + ); + type ResponseHandler = PolkadotXcm; // What happens with assets that are left in the register after the XCM message // was processed. PolkadotXcm has an AssetTrap that stores a hash of the asset diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0e3ca430d..bd8920155 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] channel = "1.74.0" -components = ["clippy", "rustfmt"] +components = ["clippy", "rust-src", "rustfmt"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/run_benches_for_pallets.sh b/scripts/run_benches_for_pallets.sh index 24e8f8d2b..4c1915d45 100755 --- a/scripts/run_benches_for_pallets.sh +++ b/scripts/run_benches_for_pallets.sh @@ -23,7 +23,9 @@ pallets=( // Add Peregrine-only pallets here! if [ "$runtime" = "peregrine" ]; then - pallets+=() + pallets+=( + pallet-asset-switch + ) fi echo "[+] Running all default weight benchmarks for $runtime --chain=$chain" diff --git a/scripts/run_benches_for_runtime.sh b/scripts/run_benches_for_runtime.sh index b7a6c333b..ba1764733 100755 --- a/scripts/run_benches_for_runtime.sh +++ b/scripts/run_benches_for_runtime.sh @@ -44,7 +44,10 @@ pallets=( # Add Peregrine-only pallets here! if [ "$runtime" = "peregrine" ]; then - pallets+=() + pallets+=( + pallet-assets + pallet-asset-switch + ) fi echo "[+] Running all runtime benchmarks for $runtime --chain=$chain"