diff --git a/Cargo.lock b/Cargo.lock index ee465188e4..dbeee5fc20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9805,6 +9805,9 @@ dependencies = [ "sp-runtime", "sp-std", "static_assertions", + "xcm", + "xcm-builder", + "xcm-executor", ] [[package]] diff --git a/runtime/acala/src/lib.rs b/runtime/acala/src/lib.rs index c264bba997..d19d56536f 100644 --- a/runtime/acala/src/lib.rs +++ b/runtime/acala/src/lib.rs @@ -103,6 +103,7 @@ pub use primitives::{ AuctionId, AuthoritysOriginId, Balance, BlockNumber, CurrencyId, DataProviderId, EraIndex, Hash, Moment, Nonce, ReserveIdentifier, Share, Signature, TokenSymbol, TradingPair, }; +use runtime_common::AcalaDropAssets; pub use runtime_common::{ cent, dollar, microcent, millicent, EnsureRootOrAllGeneralCouncil, EnsureRootOrAllTechnicalCommittee, EnsureRootOrHalfFinancialCouncil, EnsureRootOrHalfGeneralCouncil, EnsureRootOrHalfHomaCouncil, @@ -1450,7 +1451,14 @@ impl xcm_executor::Config for XcmConfig { type Weigher = FixedWeightBounds; type Trader = Trader; type ResponseHandler = PolkadotXcm; - type AssetTrap = PolkadotXcm; + type AssetTrap = AcalaDropAssets< + PolkadotXcm, + ToTreasury, + CurrencyIdConvert, + GetNativeCurrencyId, + NativeTokenExistentialDeposit, + ExistentialDeposits, + >; type AssetClaims = PolkadotXcm; type SubscriptionService = PolkadotXcm; } diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index fbf9bf7f69..b460c5c52d 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -38,6 +38,10 @@ module-transaction-payment = { path = "../../modules/transaction-payment", defau module-nft = { path = "../../modules/nft", default-features = false } module-dex = { path = "../../modules/dex", default-features = false } +xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.12", default-features = false } +xcm-executor = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.12", default-features = false } +xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.12", default-features = false } + [dev-dependencies] serde_json = "1.0.64" hex-literal = "0.3.1" @@ -87,6 +91,10 @@ std = [ "module-transaction-payment/std", "module-nft/std", "module-dex/std", + + "xcm/std", + "xcm-executor/std", + "xcm-builder/std", ] with-ethereum-compatibility = [ "module-evm/with-ethereum-compatibility", diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 5c9552df42..39d4efd6e8 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -21,6 +21,7 @@ #![cfg_attr(not(feature = "std"), no_std)] use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::Get; use frame_support::{ parameter_types, traits::Contains, @@ -49,6 +50,7 @@ mod homa; pub use homa::*; pub mod precompile; +use orml_traits::GetByKey; pub use precompile::{ AllPrecompiles, DexPrecompile, MultiCurrencyPrecompile, NFTPrecompile, OraclePrecompile, ScheduleCallPrecompile, StateRentPrecompile, @@ -57,6 +59,10 @@ pub use primitives::{ currency::{TokenInfo, ACA, AUSD, BNC, DOT, KAR, KSM, KUSD, LDOT, LKSM, PHA, RENBTC, VSKSM}, AccountId, }; +use sp_std::{marker::PhantomData, prelude::*}; +pub use xcm::latest::prelude::*; +pub use xcm_builder::TakeRevenue; +pub use xcm_executor::{traits::DropAssets, Assets}; pub type TimeStampedPrice = orml_oracle::TimestampedValue; @@ -331,6 +337,73 @@ pub enum RelayChainSubAccountId { HomaLite = 0, } +/// `DropAssets` implementation support asset amount lower thant ED handled by `TakeRevenue`. +/// +/// parameters type: +/// - `NC`: native currency_id type. +/// - `NB`: the ExistentialDeposit amount of native currency_id. +/// - `GK`: the ExistentialDeposit amount of tokens. +pub struct AcalaDropAssets(PhantomData<(X, T, C, NC, NB, GK)>); +impl DropAssets for AcalaDropAssets +where + X: DropAssets, + T: TakeRevenue, + C: Convert>, + NC: Get, + NB: Get, + GK: GetByKey, +{ + fn drop_assets(origin: &MultiLocation, assets: Assets) -> Weight { + let multi_assets: Vec = assets.into(); + let mut asset_traps: Vec = vec![]; + for asset in multi_assets { + if let MultiAsset { + id: Concrete(location), + fun: Fungible(amount), + } = asset.clone() + { + let currency_id = C::convert(location); + // burn asset(do nothing here) if convert result is None + if let Some(currency_id) = currency_id { + let ed = ExistentialDepositsForDropAssets::::get(¤cy_id); + if amount < ed { + T::take_revenue(asset); + } else { + asset_traps.push(asset); + } + } + } + } + if !asset_traps.is_empty() { + X::drop_assets(origin, asset_traps.into()); + } + 0 + } +} + +/// `ExistentialDeposit` for tokens, give priority to match native token, then handled by +/// `ExistentialDeposits`. +/// +/// parameters type: +/// - `NC`: native currency_id type. +/// - `NB`: the ExistentialDeposit amount of native currency_id. +/// - `GK`: the ExistentialDeposit amount of tokens. +pub struct ExistentialDepositsForDropAssets(PhantomData<(NC, NB, GK)>); +impl ExistentialDepositsForDropAssets +where + NC: Get, + NB: Get, + GK: GetByKey, +{ + fn get(currency_id: &CurrencyId) -> Balance { + if currency_id == &NC::get() { + NB::get() + } else { + GK::get(currency_id) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/runtime/integration-tests/src/relaychain/kusama_cross_chain_transfer.rs b/runtime/integration-tests/src/relaychain/kusama_cross_chain_transfer.rs index ec2f0770db..e63915eb3b 100644 --- a/runtime/integration-tests/src/relaychain/kusama_cross_chain_transfer.rs +++ b/runtime/integration-tests/src/relaychain/kusama_cross_chain_transfer.rs @@ -23,7 +23,7 @@ use crate::setup::*; use frame_support::assert_ok; -use karura_runtime::AssetRegistry; +use karura_runtime::{AssetRegistry, KaruraTreasuryAccount}; use module_asset_registry::AssetMetadata; use orml_traits::MultiCurrency; use xcm_emulator::TestExt; @@ -475,3 +475,178 @@ fn test_asset_registry_module() { ); }); } + +#[test] +fn trap_assets_larger_than_ed_works() { + TestNet::reset(); + + let mut kar_treasury_amount = 0; + let (ksm_asset_amount, kar_asset_amount) = (dollar(KSM), dollar(KAR)); + let trader_weight_to_treasury: u128 = 96_000_000; + + Karura::execute_with(|| { + assert_ok!(Tokens::deposit(KSM, &AccountId::from(DEFAULT), 100 * dollar(KSM))); + let _ = pallet_balances::Pallet::::deposit_creating(&AccountId::from(DEFAULT), 100 * dollar(KAR)); + + kar_treasury_amount = Currencies::free_balance(KAR, &KaruraTreasuryAccount::get()); + }); + + let assets: MultiAsset = (Parent, ksm_asset_amount).into(); + KusamaNet::execute_with(|| { + let xcm = vec![ + WithdrawAsset(assets.clone().into()), + BuyExecution { + fees: assets, + weight_limit: Limited(dollar(KSM) as u64), + }, + WithdrawAsset( + ( + (Parent, X2(Parachain(2000), GeneralKey(KAR.encode()))), + kar_asset_amount, + ) + .into(), + ), + ]; + assert_ok!(pallet_xcm::Pallet::::send_xcm( + Here, + Parachain(2000).into(), + Xcm(xcm), + )); + }); + Karura::execute_with(|| { + assert!(System::events() + .iter() + .any(|r| matches!(r.event, Event::PolkadotXcm(pallet_xcm::Event::AssetsTrapped(_, _, _))))); + + assert_eq!( + trader_weight_to_treasury, + Currencies::free_balance(KSM, &KaruraTreasuryAccount::get()) + ); + assert_eq!( + kar_treasury_amount, + Currencies::free_balance(KAR, &KaruraTreasuryAccount::get()) + ); + }); +} + +#[test] +fn trap_assets_lower_than_ed_works() { + TestNet::reset(); + + let mut kar_treasury_amount = 0; + let (ksm_asset_amount, kar_asset_amount) = (cent(KSM) / 100, cent(KAR)); + + Karura::execute_with(|| { + assert_ok!(Tokens::deposit(KSM, &AccountId::from(DEFAULT), dollar(KSM))); + let _ = pallet_balances::Pallet::::deposit_creating(&AccountId::from(DEFAULT), dollar(KAR)); + kar_treasury_amount = Currencies::free_balance(KAR, &KaruraTreasuryAccount::get()); + }); + + let assets: MultiAsset = (Parent, ksm_asset_amount).into(); + KusamaNet::execute_with(|| { + let xcm = vec![ + WithdrawAsset(assets.clone().into()), + BuyExecution { + fees: assets, + weight_limit: Limited(dollar(KSM) as u64), + }, + WithdrawAsset( + ( + (Parent, X2(Parachain(2000), GeneralKey(KAR.encode()))), + kar_asset_amount, + ) + .into(), + ), + // two asset left in holding register, they both lower than ED, so goes to treasury. + ]; + assert_ok!(pallet_xcm::Pallet::::send_xcm( + Here, + Parachain(2000).into(), + Xcm(xcm), + )); + }); + + Karura::execute_with(|| { + assert_eq!( + System::events() + .iter() + .find(|r| matches!(r.event, Event::PolkadotXcm(pallet_xcm::Event::AssetsTrapped(_, _, _)))), + None + ); + + assert_eq!( + ksm_asset_amount, + Currencies::free_balance(KSM, &KaruraTreasuryAccount::get()) + ); + assert_eq!( + kar_asset_amount, + Currencies::free_balance(KAR, &KaruraTreasuryAccount::get()) - kar_treasury_amount + ); + }); +} + +#[test] +fn sibling_trap_assets_works() { + TestNet::reset(); + + let mut kar_treasury_amount = 0; + let (bnc_asset_amount, kar_asset_amount) = (cent(BNC) / 10, cent(KAR)); + + fn sibling_account() -> AccountId { + use sp_runtime::traits::AccountIdConversion; + polkadot_parachain::primitives::Sibling::from(2001).into_account() + } + + Karura::execute_with(|| { + assert_ok!(Tokens::deposit(BNC, &sibling_account(), dollar(BNC))); + let _ = pallet_balances::Pallet::::deposit_creating(&sibling_account(), dollar(KAR)); + kar_treasury_amount = Currencies::free_balance(KAR, &KaruraTreasuryAccount::get()); + }); + + Sibling::execute_with(|| { + let assets: MultiAsset = ( + (Parent, X2(Parachain(2000), GeneralKey(KAR.encode()))), + kar_asset_amount, + ) + .into(); + let xcm = vec![ + WithdrawAsset(assets.clone().into()), + BuyExecution { + fees: assets, + weight_limit: Unlimited, + }, + WithdrawAsset( + ( + ( + Parent, + X2(Parachain(2001), GeneralKey(parachains::bifrost::BNC_KEY.to_vec())), + ), + bnc_asset_amount, + ) + .into(), + ), + ]; + assert_ok!(pallet_xcm::Pallet::::send_xcm( + Here, + (Parent, Parachain(2000)), + Xcm(xcm), + )); + }); + + Karura::execute_with(|| { + assert_eq!( + System::events() + .iter() + .find(|r| matches!(r.event, Event::PolkadotXcm(pallet_xcm::Event::AssetsTrapped(_, _, _)))), + None + ); + assert_eq!( + Currencies::free_balance(KAR, &KaruraTreasuryAccount::get()) - kar_treasury_amount, + kar_asset_amount + ); + assert_eq!( + Currencies::free_balance(BNC, &KaruraTreasuryAccount::get()), + bnc_asset_amount + ); + }); +} diff --git a/runtime/integration-tests/src/setup.rs b/runtime/integration-tests/src/setup.rs index b0590e8a84..3734caa6c8 100644 --- a/runtime/integration-tests/src/setup.rs +++ b/runtime/integration-tests/src/setup.rs @@ -150,6 +150,7 @@ const ORACLE3: [u8; 32] = [2u8; 32]; const ORACLE4: [u8; 32] = [3u8; 32]; const ORACLE5: [u8; 32] = [4u8; 32]; +pub const DEFAULT: [u8; 32] = [0u8; 32]; pub const ALICE: [u8; 32] = [4u8; 32]; pub const BOB: [u8; 32] = [5u8; 32]; pub const CHARLIE: [u8; 32] = [6u8; 32]; diff --git a/runtime/karura/src/lib.rs b/runtime/karura/src/lib.rs index 935eec6fec..5bffbb1b5d 100644 --- a/runtime/karura/src/lib.rs +++ b/runtime/karura/src/lib.rs @@ -104,6 +104,7 @@ pub use primitives::{ AuctionId, AuthoritysOriginId, Balance, BlockNumber, CurrencyId, DataProviderId, EraIndex, Hash, Moment, Nonce, ReserveIdentifier, Share, Signature, TokenSymbol, TradingPair, }; +use runtime_common::AcalaDropAssets; pub use runtime_common::{ cent, dollar, microcent, millicent, EnsureRootOrAllGeneralCouncil, EnsureRootOrAllTechnicalCommittee, EnsureRootOrHalfFinancialCouncil, EnsureRootOrHalfGeneralCouncil, EnsureRootOrHalfHomaCouncil, @@ -1513,7 +1514,14 @@ impl xcm_executor::Config for XcmConfig { type Weigher = FixedWeightBounds; type Trader = Trader; type ResponseHandler = PolkadotXcm; - type AssetTrap = PolkadotXcm; + type AssetTrap = AcalaDropAssets< + PolkadotXcm, + ToTreasury, + CurrencyIdConvert, + GetNativeCurrencyId, + NativeTokenExistentialDeposit, + ExistentialDeposits, + >; type AssetClaims = PolkadotXcm; type SubscriptionService = PolkadotXcm; } diff --git a/runtime/mandala/src/lib.rs b/runtime/mandala/src/lib.rs index 579df2c9dc..2244efe379 100644 --- a/runtime/mandala/src/lib.rs +++ b/runtime/mandala/src/lib.rs @@ -127,6 +127,7 @@ pub use runtime_common::{ /// Import the stable_asset pallet. pub use nutsfinance_stable_asset; +use runtime_common::AcalaDropAssets; mod authority; mod benchmarking; @@ -1677,7 +1678,14 @@ impl xcm_executor::Config for XcmConfig { // Only receiving DOT is handled, and all fees must be paid in DOT. type Trader = Trader; type ResponseHandler = (); // Don't handle responses for now. - type AssetTrap = (); + type AssetTrap = AcalaDropAssets< + PolkadotXcm, + ToTreasury, + CurrencyIdConvert, + GetNativeCurrencyId, + NativeTokenExistentialDeposit, + ExistentialDeposits, + >; type AssetClaims = (); type SubscriptionService = PolkadotXcm; }