diff --git a/docs/changelog_for_devs.md b/docs/changelog_for_devs.md index 96069327f..1f25cc278 100644 --- a/docs/changelog_for_devs.md +++ b/docs/changelog_for_devs.md @@ -12,6 +12,23 @@ https://keepachangelog.com/en/1.0.0/ and ⚠️ marks changes that might break components which query the chain's storage, the extrinsics or the runtime APIs/RPC interface. +## v0.3.11 + +[#1049]: https://github.com/zeitgeistpm/zeitgeist/pull/1049 + +### Changed + +- ⚠️ All tokens now use 10 fractional decimal places. + - cross-consensus messages (XCM) assume the global canonical representation for token balances. +- The token metadata in the asset registry now assumes that the existential deposit and fee factor + are stored in base 10,000,000,000. + +### Added + +- Use pallet-asset-tx-payment for allowing to pay transaction fees in foreign + currencies ([#1022]). This requires each transaction to specify the fee + payment token with `asset_id` (`None` is ZTG). + ## v0.3.10 [#1022]: https://github.com/zeitgeistpm/zeitgeist/pull/1022 diff --git a/runtime/battery-station/src/integration_tests/xcm/setup.rs b/runtime/battery-station/src/integration_tests/xcm/setup.rs index 83cae024f..e8a57c2db 100644 --- a/runtime/battery-station/src/integration_tests/xcm/setup.rs +++ b/runtime/battery-station/src/integration_tests/xcm/setup.rs @@ -107,6 +107,41 @@ pub const PARA_ID_SIBLING: u32 = 3000; pub const FOREIGN_ZTG_ID: Asset = CurrencyId::ForeignAsset(0); pub const FOREIGN_PARENT_ID: Asset = CurrencyId::ForeignAsset(1); pub const FOREIGN_SIBLING_ID: Asset = CurrencyId::ForeignAsset(2); +pub const BTC_ID: Asset = CurrencyId::ForeignAsset(3); + +#[inline] +pub(super) const fn ztg(amount: Balance) -> Balance { + amount * dollar(10) +} + +#[inline] +pub(super) const fn roc(amount: Balance) -> Balance { + foreign(amount, 12) +} + +#[inline] +pub(super) const fn btc(amount: Balance) -> Balance { + foreign(amount, 8) +} + +#[inline] +pub(super) const fn foreign(amount: Balance, decimals: u32) -> Balance { + amount * dollar(decimals) +} + +#[inline] +pub(super) const fn dollar(decimals: u32) -> Balance { + 10u128.saturating_pow(decimals) +} + +#[inline] +pub(super) const fn adjusted_balance(foreign_base: Balance, amount: Balance) -> Balance { + if foreign_base > ztg(1) { + amount.saturating_div(foreign_base / ztg(1)) + } else { + amount.saturating_mul(ztg(1) / foreign_base) + } +} // Multilocations that are used to represent tokens from other chains #[inline] @@ -138,6 +173,19 @@ pub(super) fn register_foreign_ztg(additional_meta: Option) { assert_ok!(AssetRegistry::register_asset(RuntimeOrigin::root(), meta, Some(FOREIGN_ZTG_ID))); } +pub(super) fn register_btc(additional_meta: Option) { + let meta: AssetMetadata = AssetMetadata { + decimals: 8, + name: "Bitcoin".into(), + symbol: "BTC".into(), + existential_deposit: ExistentialDeposit::get(), + location: Some(VersionedMultiLocation::V1(foreign_sibling_multilocation())), + additional: additional_meta.unwrap_or_default(), + }; + + assert_ok!(AssetRegistry::register_asset(RuntimeOrigin::root(), meta, Some(BTC_ID))); +} + pub(super) fn register_foreign_sibling(additional_meta: Option) { // Register native Sibling token as foreign asset. let meta: AssetMetadata = AssetMetadata { @@ -170,26 +218,6 @@ pub(super) fn register_foreign_parent(additional_meta: Option) { assert_ok!(AssetRegistry::register_asset(RuntimeOrigin::root(), meta, Some(FOREIGN_PARENT_ID))); } -#[inline] -pub(super) fn ztg(amount: Balance) -> Balance { - amount * dollar(10) -} - -#[inline] -pub(super) fn roc(amount: Balance) -> Balance { - foreign(amount, 12) -} - -#[inline] -pub(super) fn foreign(amount: Balance, decimals: u32) -> Balance { - amount * dollar(decimals) -} - -#[inline] -pub(super) fn dollar(decimals: u32) -> Balance { - 10u128.saturating_pow(decimals) -} - #[inline] pub(super) fn sibling_parachain_account() -> AccountId { parachain_account(PARA_ID_SIBLING) diff --git a/runtime/battery-station/src/integration_tests/xcm/tests/transfers.rs b/runtime/battery-station/src/integration_tests/xcm/tests/transfers.rs index 65514f572..08ab33927 100644 --- a/runtime/battery-station/src/integration_tests/xcm/tests/transfers.rs +++ b/runtime/battery-station/src/integration_tests/xcm/tests/transfers.rs @@ -19,9 +19,9 @@ use crate::{ integration_tests::xcm::{ setup::{ - register_foreign_parent, register_foreign_ztg, roc, sibling_parachain_account, - zeitgeist_parachain_account, ztg, ALICE, BOB, FOREIGN_PARENT_ID, FOREIGN_ZTG_ID, - PARA_ID_SIBLING, + adjusted_balance, btc, register_btc, register_foreign_parent, register_foreign_ztg, + roc, sibling_parachain_account, zeitgeist_parachain_account, ztg, ALICE, BOB, BTC_ID, + FOREIGN_PARENT_ID, FOREIGN_ZTG_ID, PARA_ID_SIBLING, }, test_net::{RococoNet, Sibling, TestNet, Zeitgeist}, }, @@ -33,7 +33,7 @@ use crate::{ use frame_support::assert_ok; use orml_traits::MultiCurrency; use xcm::latest::{Junction, Junction::*, Junctions::*, MultiLocation, NetworkId}; -use xcm_emulator::TestExt; +use xcm_emulator::{Limited, TestExt}; use zeitgeist_primitives::{ constants::BalanceFractionalDecimals, types::{CustomMetadata, XcmMetadata}, @@ -71,7 +71,7 @@ fn transfer_ztg_to_sibling() { ) .into() ), - xcm_emulator::Limited(4_000_000_000), + Limited(4_000_000_000), )); // Confirm that Alice's balance is initial_balance - amount_transferred @@ -141,7 +141,7 @@ fn transfer_ztg_sibling_to_zeitgeist() { ) .into() ), - xcm_emulator::Limited(4_000_000_000), + Limited(4_000_000_000), )); // Confirm that Bobs's balance is initial balance - amount transferred @@ -172,6 +172,121 @@ fn transfer_ztg_sibling_to_zeitgeist() { }); } +#[test] +fn transfer_btc_sibling_to_zeitgeist() { + TestNet::reset(); + + let sibling_alice_initial_balance = ztg(10); + let zeitgeist_alice_initial_balance = btc(0); + let initial_sovereign_balance = btc(100); + let transfer_amount = btc(100); + + Zeitgeist::execute_with(|| { + register_btc(None); + + assert_eq!(Tokens::free_balance(BTC_ID, &ALICE), zeitgeist_alice_initial_balance,); + }); + + Sibling::execute_with(|| { + assert_eq!(Balances::free_balance(&ALICE), sibling_alice_initial_balance); + // Set the sovereign balance such that it is not subject to dust collection + assert_ok!(Balances::set_balance( + RuntimeOrigin::root(), + zeitgeist_parachain_account().into(), + initial_sovereign_balance, + 0 + )); + assert_ok!(XTokens::transfer( + RuntimeOrigin::signed(ALICE), + // Target chain will interpret CurrencyId::Ztg as BTC in this context. + CurrencyId::Ztg, + transfer_amount, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(battery_station::ID), + Junction::AccountId32 { network: NetworkId::Any, id: ALICE.into() } + ) + ) + .into() + ), + Limited(4_000_000_000), + )); + + // Confirm that Alice's balance is initial_balance - amount_transferred + assert_eq!(Balances::free_balance(&ALICE), sibling_alice_initial_balance - transfer_amount); + + // Verify that the amount transferred is now part of the zeitgeist account here + assert_eq!( + Balances::free_balance(zeitgeist_parachain_account()), + initial_sovereign_balance + transfer_amount + ); + }); + + Zeitgeist::execute_with(|| { + let expected = transfer_amount - btc_fee(); + let expected_adjusted = adjusted_balance(btc(1), expected); + + // Verify that remote Alice now has initial balance + amount transferred - fee + assert_eq!( + Tokens::free_balance(BTC_ID, &ALICE), + zeitgeist_alice_initial_balance + expected_adjusted, + ); + }); +} + +#[test] +fn transfer_btc_zeitgeist_to_sibling() { + TestNet::reset(); + + let transfer_amount = btc(100) - btc_fee(); + let initial_sovereign_balance = 2 * btc(100); + let sibling_bob_initial_balance = btc(0); + + transfer_btc_sibling_to_zeitgeist(); + + Sibling::execute_with(|| { + assert_eq!(Tokens::free_balance(BTC_ID, &BOB), sibling_bob_initial_balance,); + }); + + Zeitgeist::execute_with(|| { + assert_ok!(XTokens::transfer( + RuntimeOrigin::signed(ALICE), + BTC_ID, + transfer_amount, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(PARA_ID_SIBLING), + Junction::AccountId32 { network: NetworkId::Any, id: BOB.into() } + ) + ) + .into() + ), + Limited(4_000_000_000), + )); + + // Confirm that Alice's balance is initial_balance - amount_transferred + assert_eq!(Tokens::free_balance(BTC_ID, &ALICE), 0); + }); + + Sibling::execute_with(|| { + let fee_adjusted = adjusted_balance(btc(1), btc_fee()); + let expected = transfer_amount - fee_adjusted; + + // Verify that Bob now has initial balance + amount transferred - fee + assert_eq!(Balances::free_balance(&BOB), sibling_bob_initial_balance + expected,); + + // Verify that the amount transferred is now subtracted from the zeitgeist account at sibling + assert_eq!( + Balances::free_balance(zeitgeist_parachain_account()), + initial_sovereign_balance - transfer_amount + ); + }); +} + #[test] fn transfer_roc_from_relay_chain() { TestNet::reset(); @@ -198,7 +313,9 @@ fn transfer_roc_from_relay_chain() { }); Zeitgeist::execute_with(|| { - assert_eq!(Tokens::free_balance(FOREIGN_PARENT_ID, &BOB), transfer_amount - roc_fee()); + let expected = transfer_amount - roc_fee(); + let expected_adjusted = adjusted_balance(roc(1), expected); + assert_eq!(Tokens::free_balance(FOREIGN_PARENT_ID, &BOB), expected_adjusted); }); } @@ -207,6 +324,7 @@ fn transfer_roc_to_relay_chain() { TestNet::reset(); let transfer_amount: Balance = roc(1); + let transfer_amount_local: Balance = adjusted_balance(roc(1), transfer_amount); transfer_roc_from_relay_chain(); Zeitgeist::execute_with(|| { @@ -224,12 +342,12 @@ fn transfer_roc_to_relay_chain() { ) .into() ), - xcm_emulator::Limited(4_000_000_000) + Limited(4_000_000_000) )); assert_eq!( Tokens::free_balance(FOREIGN_PARENT_ID, &ALICE), - initial_balance - transfer_amount + initial_balance - transfer_amount_local ) }); @@ -286,7 +404,7 @@ fn transfer_ztg_to_sibling_with_custom_fee() { ) .into() ), - xcm_emulator::Limited(4_000_000_000), + Limited(4_000_000_000), )); // Confirm that Alice's balance is initial_balance - amount_transferred @@ -337,7 +455,12 @@ fn roc_fee() -> Balance { } #[inline] -fn calc_fee(fee_per_second: Balance) -> Balance { +fn btc_fee() -> Balance { + fee(8) +} + +#[inline] +const fn calc_fee(fee_per_second: Balance) -> Balance { // We divide the fee to align its unit and multiply by 8 as that seems to be the unit of // time the tests take. // NOTE: it is possible that in different machines this value may differ. We shall see. diff --git a/runtime/battery-station/src/xcm_config/config.rs b/runtime/battery-station/src/xcm_config/config.rs index 003801fda..d9d437409 100644 --- a/runtime/battery-station/src/xcm_config/config.rs +++ b/runtime/battery-station/src/xcm_config/config.rs @@ -22,9 +22,14 @@ use crate::{ RuntimeOrigin, UnitWeightCost, UnknownTokens, XcmpQueue, ZeitgeistTreasuryAccount, }; -use frame_support::{parameter_types, traits::Everything, WeakBoundedVec}; +use core::marker::PhantomData; +use frame_support::{ + parameter_types, + traits::{ConstU8, Everything, Get}, + WeakBoundedVec, +}; use orml_asset_registry::{AssetRegistryTrader, FixedRateAssetRegistryTrader}; -use orml_traits::{location::AbsoluteReserveProvider, MultiCurrency}; +use orml_traits::{asset_registry::Inspect, location::AbsoluteReserveProvider, MultiCurrency}; use orml_xcm_support::{ DepositToAlternative, IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset, }; @@ -34,7 +39,7 @@ use sp_runtime::traits::Convert; use xcm::{ latest::{ prelude::{AccountId32, AssetId, Concrete, GeneralKey, MultiAsset, NetworkId, X1, X2}, - Junction, MultiLocation, + Error as XcmError, Junction, MultiLocation, Result as XcmResult, }, opaque::latest::Fungibility::Fungible, }; @@ -45,8 +50,8 @@ use xcm_builder::{ SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeRevenue, TakeWeightCredit, }; -use xcm_executor::Config; -use zeitgeist_primitives::types::Asset; +use xcm_executor::{traits::TransactAsset, Assets, Config}; +use zeitgeist_primitives::{constants::BalanceFractionalDecimals, types::Asset}; pub mod battery_station { #[cfg(test)] @@ -63,7 +68,7 @@ impl Config for XcmConfig { /// The handler for when there is an instruction to claim assets. type AssetClaims = PolkadotXcm; /// How to withdraw and deposit an asset. - type AssetTransactor = MultiAssetTransactor; + type AssetTransactor = AlignedFractionalMultiAssetTransactor; /// The general asset trap - handler for when assets are left in the Holding Register at the /// end of execution. type AssetTrap = PolkadotXcm; @@ -158,6 +163,108 @@ parameter_types! { ); } +/// A generic warpper around implementations of the (xcm-executor) `TransactAsset` trait. +/// +/// Aligns the fractional decimal places of every incoming token with ZTG. +/// Reconstructs the original number of fractional decimal places of every outgoing token. +/// +/// Important: Always use the global canonical representation of token balances in XCM. +/// Only during the interpretation of those XCM adjustments happens. +/// +/// Important: The implementation does not support teleports. +#[allow(clippy::type_complexity)] +pub struct AlignedFractionalTransactAsset< + AssetRegistry, + CurrencyIdConvert, + FracDecPlaces, + TransactAssetDelegate, +> { + _phantom: PhantomData<(AssetRegistry, CurrencyIdConvert, FracDecPlaces, TransactAssetDelegate)>, +} + +impl< + AssetRegistry: Inspect, + FracDecPlaces: Get, + CurrencyIdConvert: Convert>, + TransactAssetDelegate: TransactAsset, +> + AlignedFractionalTransactAsset< + AssetRegistry, + CurrencyIdConvert, + FracDecPlaces, + TransactAssetDelegate, + > +{ + fn adjust_fractional_places(asset: &MultiAsset) -> MultiAsset { + if let Some(ref asset_id) = CurrencyIdConvert::convert(asset.clone()) { + if let Fungible(amount) = asset.fun { + let mut asset_updated = asset.clone(); + let native_decimals = u32::from(FracDecPlaces::get()); + let metadata = AssetRegistry::metadata(asset_id); + + if let Some(metadata) = metadata { + let decimals = metadata.decimals; + + asset_updated.fun = if decimals > native_decimals { + let power = decimals.saturating_sub(native_decimals); + let adjust_factor = 10u128.saturating_pow(power); + // Floors the adjusted token amount, thus no tokens are generated + Fungible(amount.saturating_div(adjust_factor)) + } else { + let power = native_decimals.saturating_sub(decimals); + let adjust_factor = 10u128.saturating_pow(power); + Fungible(amount.saturating_mul(adjust_factor)) + }; + + return asset_updated; + } + } + } + + asset.clone() + } +} + +impl< + AssetRegistry: Inspect, + CurrencyIdConvert: Convert>, + FracDecPlaces: Get, + TransactAssetDelegate: TransactAsset, +> TransactAsset + for AlignedFractionalTransactAsset< + AssetRegistry, + CurrencyIdConvert, + FracDecPlaces, + TransactAssetDelegate, + > +{ + fn deposit_asset(asset: &MultiAsset, location: &MultiLocation) -> XcmResult { + let asset_adjusted = Self::adjust_fractional_places(asset); + TransactAssetDelegate::deposit_asset(&asset_adjusted, location) + } + + fn withdraw_asset(asset: &MultiAsset, location: &MultiLocation) -> Result { + let asset_adjusted = Self::adjust_fractional_places(asset); + TransactAssetDelegate::withdraw_asset(&asset_adjusted, location) + } + + fn transfer_asset( + asset: &MultiAsset, + from: &MultiLocation, + to: &MultiLocation, + ) -> Result { + let asset_adjusted = Self::adjust_fractional_places(asset); + TransactAssetDelegate::transfer_asset(&asset_adjusted, from, to) + } +} + +pub type AlignedFractionalMultiAssetTransactor = AlignedFractionalTransactAsset< + AssetRegistry, + AssetConvert, + ConstU8<{ BalanceFractionalDecimals::get() }>, + MultiAssetTransactor, +>; + /// Means for transacting assets on this chain. pub type MultiAssetTransactor = MultiCurrencyAdapter< // All known Assets will be processed by the following MultiCurrency implementation. diff --git a/runtime/battery-station/src/xcm_config/fees.rs b/runtime/battery-station/src/xcm_config/fees.rs index 53a036e53..158ddcb18 100644 --- a/runtime/battery-station/src/xcm_config/fees.rs +++ b/runtime/battery-station/src/xcm_config/fees.rs @@ -21,7 +21,7 @@ use core::marker::PhantomData; use frame_support::weights::constants::{ExtrinsicBaseWeight, WEIGHT_PER_SECOND}; use xcm::latest::MultiLocation; use zeitgeist_primitives::{constants::BalanceFractionalDecimals, types::CustomMetadata}; -use zrml_swaps::check_arithm_rslt::CheckArithmRslt; +use zrml_swaps::fixed::bmul; /// The fee cost per second for transferring the native token in cents. pub fn native_per_second() -> Balance { @@ -48,14 +48,9 @@ pub fn cent(decimals: u32) -> Balance { dollar(decimals).saturating_div(100) } -pub fn bmul(a: u128, b: u128, base: u128) -> Option { - let c0 = a.check_mul_rslt(&b).ok()?; - let c1 = c0.check_add_rslt(&base.check_div_rslt(&2).ok()?).ok()?; - c1.check_div_rslt(&base).ok() -} - -/// Our FixedConversionRateProvider, used to charge XCM-related fees for tokens registered in +/// The FixedConversionRateProvider is used to charge XCM-related fees for tokens registered in /// the asset registry that were not already handled by native Trader rules. +/// Assumes that the fee factor is stored in the native base. pub struct FixedConversionRateProvider(PhantomData); impl< @@ -68,13 +63,22 @@ impl< { fn get_fee_per_second(location: &MultiLocation) -> Option { let metadata = AssetRegistry::metadata_by_location(location)?; - let default_per_second = default_per_second(metadata.decimals); + let default_per_second = native_per_second(); + let native_decimals: u32 = BalanceFractionalDecimals::get().into(); + let foreign_decimals = metadata.decimals; + + let fee_unadjusted = if let Some(fee_factor) = metadata.additional.xcm.fee_factor { + bmul(default_per_second, fee_factor).ok()? + } else { + default_per_second + }; - if let Some(fee_factor) = metadata.additional.xcm.fee_factor { - let base = 10u128.checked_pow(metadata.decimals)?; - bmul(default_per_second, fee_factor, base) + if native_decimals > foreign_decimals { + let power = native_decimals.saturating_sub(foreign_decimals); + Some(fee_unadjusted.checked_div(10u128.checked_pow(power)?)?) } else { - Some(default_per_second) + let power = foreign_decimals.saturating_sub(native_decimals); + Some(fee_unadjusted.checked_mul(10u128.checked_pow(power)?)?) } } } diff --git a/runtime/zeitgeist/src/integration_tests/xcm/setup.rs b/runtime/zeitgeist/src/integration_tests/xcm/setup.rs index aa717bf53..771f178ff 100644 --- a/runtime/zeitgeist/src/integration_tests/xcm/setup.rs +++ b/runtime/zeitgeist/src/integration_tests/xcm/setup.rs @@ -107,6 +107,47 @@ pub const PARA_ID_SIBLING: u32 = 3000; pub const FOREIGN_ZTG_ID: Asset = CurrencyId::ForeignAsset(0); pub const FOREIGN_PARENT_ID: Asset = CurrencyId::ForeignAsset(1); pub const FOREIGN_SIBLING_ID: Asset = CurrencyId::ForeignAsset(2); +pub const BTC_ID: Asset = CurrencyId::ForeignAsset(3); +pub const ETH_ID: Asset = CurrencyId::ForeignAsset(4); + +#[inline] +pub(super) const fn ztg(amount: Balance) -> Balance { + amount * dollar(10) +} + +#[inline] +pub(super) const fn dot(amount: Balance) -> Balance { + foreign(amount, 10) +} + +#[inline] +pub(super) const fn btc(amount: Balance) -> Balance { + foreign(amount, 8) +} + +#[inline] +pub(super) const fn eth(amount: Balance) -> Balance { + foreign(amount, 18) +} + +#[inline] +pub(super) const fn foreign(amount: Balance, decimals: u32) -> Balance { + amount * dollar(decimals) +} + +#[inline] +pub(super) const fn dollar(decimals: u32) -> Balance { + 10u128.saturating_pow(decimals) +} + +#[inline] +pub(super) const fn adjusted_balance(foreign_base: Balance, amount: Balance) -> Balance { + if foreign_base > ztg(1) { + amount.saturating_div(foreign_base / ztg(1)) + } else { + amount.saturating_mul(ztg(1) / foreign_base) + } +} // Multilocations that are used to represent tokens from other chains #[inline] @@ -138,6 +179,32 @@ pub(super) fn register_foreign_ztg(additional_meta: Option) { assert_ok!(AssetRegistry::register_asset(RuntimeOrigin::root(), meta, Some(FOREIGN_ZTG_ID))); } +pub(super) fn register_btc(additional_meta: Option) { + let meta: AssetMetadata = AssetMetadata { + decimals: 8, + name: "Bitcoin".into(), + symbol: "BTC".into(), + existential_deposit: ExistentialDeposit::get(), + location: Some(VersionedMultiLocation::V1(foreign_sibling_multilocation())), + additional: additional_meta.unwrap_or_default(), + }; + + assert_ok!(AssetRegistry::register_asset(RuntimeOrigin::root(), meta, Some(BTC_ID))); +} + +pub(super) fn register_eth(additional_meta: Option) { + let meta: AssetMetadata = AssetMetadata { + decimals: 18, + name: "Ethereum".into(), + symbol: "ETH".into(), + existential_deposit: ExistentialDeposit::get(), + location: Some(VersionedMultiLocation::V1(foreign_sibling_multilocation())), + additional: additional_meta.unwrap_or_default(), + }; + + assert_ok!(AssetRegistry::register_asset(RuntimeOrigin::root(), meta, Some(ETH_ID))); +} + pub(super) fn register_foreign_sibling(additional_meta: Option) { // Register native Sibling token as foreign asset. let meta: AssetMetadata = AssetMetadata { @@ -170,26 +237,6 @@ pub(super) fn register_foreign_parent(additional_meta: Option) { assert_ok!(AssetRegistry::register_asset(RuntimeOrigin::root(), meta, Some(FOREIGN_PARENT_ID))); } -#[inline] -pub(super) fn ztg(amount: Balance) -> Balance { - amount * dollar(10) -} - -#[inline] -pub(super) fn dot(amount: Balance) -> Balance { - foreign(amount, 10) -} - -#[inline] -pub(super) fn foreign(amount: Balance, decimals: u32) -> Balance { - amount * dollar(decimals) -} - -#[inline] -pub(super) fn dollar(decimals: u32) -> Balance { - 10u128.saturating_pow(decimals) -} - #[inline] pub(super) fn sibling_parachain_account() -> AccountId { parachain_account(PARA_ID_SIBLING) diff --git a/runtime/zeitgeist/src/integration_tests/xcm/tests/transfers.rs b/runtime/zeitgeist/src/integration_tests/xcm/tests/transfers.rs index bf17fc2a0..60b4c6137 100644 --- a/runtime/zeitgeist/src/integration_tests/xcm/tests/transfers.rs +++ b/runtime/zeitgeist/src/integration_tests/xcm/tests/transfers.rs @@ -19,9 +19,9 @@ use crate::{ integration_tests::xcm::{ setup::{ - dot, register_foreign_parent, register_foreign_ztg, sibling_parachain_account, - zeitgeist_parachain_account, ztg, ALICE, BOB, FOREIGN_PARENT_ID, FOREIGN_ZTG_ID, - PARA_ID_SIBLING, + adjusted_balance, btc, dot, eth, register_btc, register_eth, register_foreign_parent, + register_foreign_ztg, sibling_parachain_account, zeitgeist_parachain_account, ztg, + ALICE, BOB, BTC_ID, ETH_ID, FOREIGN_PARENT_ID, FOREIGN_ZTG_ID, PARA_ID_SIBLING, }, test_net::{PolkadotNet, Sibling, TestNet, Zeitgeist}, }, @@ -33,7 +33,7 @@ use crate::{ use frame_support::assert_ok; use orml_traits::MultiCurrency; use xcm::latest::{Junction, Junction::*, Junctions::*, MultiLocation, NetworkId}; -use xcm_emulator::TestExt; +use xcm_emulator::{Limited, TestExt}; use zeitgeist_primitives::{ constants::BalanceFractionalDecimals, types::{CustomMetadata, XcmMetadata}, @@ -71,7 +71,7 @@ fn transfer_ztg_to_sibling() { ) .into() ), - xcm_emulator::Limited(4_000_000_000), + Limited(4_000_000_000), )); // Confirm that Alice's balance is initial_balance - amount_transferred @@ -141,7 +141,7 @@ fn transfer_ztg_sibling_to_zeitgeist() { ) .into() ), - xcm_emulator::Limited(4_000_000_000), + Limited(4_000_000_000), )); // Confirm that Bobs's balance is initial balance - amount transferred @@ -172,6 +172,242 @@ fn transfer_ztg_sibling_to_zeitgeist() { }); } +#[test] +fn transfer_btc_sibling_to_zeitgeist() { + TestNet::reset(); + + let sibling_alice_initial_balance = ztg(10); + let zeitgeist_alice_initial_balance = btc(0); + let initial_sovereign_balance = btc(100); + let transfer_amount = btc(100); + + Zeitgeist::execute_with(|| { + register_btc(None); + + assert_eq!(Tokens::free_balance(BTC_ID, &ALICE), zeitgeist_alice_initial_balance,); + }); + + Sibling::execute_with(|| { + assert_eq!(Balances::free_balance(&ALICE), sibling_alice_initial_balance); + // Set the sovereign balance such that it is not subject to dust collection + assert_ok!(Balances::set_balance( + RuntimeOrigin::root(), + zeitgeist_parachain_account().into(), + initial_sovereign_balance, + 0 + )); + assert_ok!(XTokens::transfer( + RuntimeOrigin::signed(ALICE), + // Target chain will interpret CurrencyId::Ztg as BTC in this context. + CurrencyId::Ztg, + transfer_amount, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(zeitgeist::ID), + Junction::AccountId32 { network: NetworkId::Any, id: ALICE.into() } + ) + ) + .into() + ), + Limited(4_000_000_000), + )); + + // Confirm that Alice's balance is initial_balance - amount_transferred + assert_eq!(Balances::free_balance(&ALICE), sibling_alice_initial_balance - transfer_amount); + + // Verify that the amount transferred is now part of the zeitgeist account here + assert_eq!( + Balances::free_balance(zeitgeist_parachain_account()), + initial_sovereign_balance + transfer_amount + ); + }); + + Zeitgeist::execute_with(|| { + let expected = transfer_amount - btc_fee(); + let expected_adjusted = adjusted_balance(btc(1), expected); + + // Verify that remote Alice now has initial balance + amount transferred - fee + assert_eq!( + Tokens::free_balance(BTC_ID, &ALICE), + zeitgeist_alice_initial_balance + expected_adjusted, + ); + }); +} + +#[test] +fn transfer_btc_zeitgeist_to_sibling() { + TestNet::reset(); + + let transfer_amount = btc(100) - btc_fee(); + let initial_sovereign_balance = 2 * btc(100); + let sibling_bob_initial_balance = btc(0); + + transfer_btc_sibling_to_zeitgeist(); + + Sibling::execute_with(|| { + assert_eq!(Tokens::free_balance(BTC_ID, &BOB), sibling_bob_initial_balance,); + }); + + Zeitgeist::execute_with(|| { + assert_ok!(XTokens::transfer( + RuntimeOrigin::signed(ALICE), + BTC_ID, + transfer_amount, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(PARA_ID_SIBLING), + Junction::AccountId32 { network: NetworkId::Any, id: BOB.into() } + ) + ) + .into() + ), + Limited(4_000_000_000), + )); + + // Confirm that Alice's balance is initial_balance - amount_transferred + assert_eq!(Tokens::free_balance(BTC_ID, &ALICE), 0); + }); + + Sibling::execute_with(|| { + let fee_adjusted = adjusted_balance(btc(1), btc_fee()); + let expected = transfer_amount - fee_adjusted; + + // Verify that Bob now has initial balance + amount transferred - fee + assert_eq!(Balances::free_balance(&BOB), sibling_bob_initial_balance + expected,); + + // Verify that the amount transferred is now subtracted from the zeitgeist account at sibling + assert_eq!( + Balances::free_balance(zeitgeist_parachain_account()), + initial_sovereign_balance - transfer_amount + ); + }); +} + +#[test] +fn transfer_eth_sibling_to_zeitgeist() { + TestNet::reset(); + + let sibling_alice_initial_balance = ztg(10) + eth(1); + let zeitgeist_alice_initial_balance = eth(0); + let initial_sovereign_balance = eth(1); + let transfer_amount = eth(1); + + Zeitgeist::execute_with(|| { + register_eth(None); + + assert_eq!(Tokens::free_balance(ETH_ID, &ALICE), zeitgeist_alice_initial_balance,); + }); + + Sibling::execute_with(|| { + // Set the sovereign balance such that it is not subject to dust collection + assert_ok!(Balances::set_balance( + RuntimeOrigin::root(), + zeitgeist_parachain_account().into(), + initial_sovereign_balance, + 0 + )); + // Add 1 "fake" ETH to Alice's balance + assert_ok!(Balances::set_balance( + RuntimeOrigin::root(), + ALICE.into(), + sibling_alice_initial_balance, + 0 + )); + assert_ok!(XTokens::transfer( + RuntimeOrigin::signed(ALICE), + // Target chain will interpret CurrencyId::Ztg as ETH in this context. + CurrencyId::Ztg, + transfer_amount, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(zeitgeist::ID), + Junction::AccountId32 { network: NetworkId::Any, id: ALICE.into() } + ) + ) + .into() + ), + Limited(4_000_000_000), + )); + + // Confirm that Alice's balance is initial_balance - amount_transferred + assert_eq!(Balances::free_balance(&ALICE), sibling_alice_initial_balance - transfer_amount); + + // Verify that the amount transferred is now part of the zeitgeist account here + assert_eq!( + Balances::free_balance(zeitgeist_parachain_account()), + initial_sovereign_balance + transfer_amount + ); + }); + + Zeitgeist::execute_with(|| { + let expected = transfer_amount - eth_fee(); + let expected_adjusted = adjusted_balance(eth(1), expected); + + // Verify that remote Alice now has initial balance + amount transferred - fee + assert_eq!( + Tokens::free_balance(ETH_ID, &ALICE), + zeitgeist_alice_initial_balance + expected_adjusted, + ); + }); +} + +#[test] +fn transfer_eth_zeitgeist_to_sibling() { + TestNet::reset(); + + let transfer_amount = eth(1) - eth_fee(); + let initial_sovereign_balance = 2 * eth(1); + let sibling_bob_initial_balance = eth(0); + + transfer_eth_sibling_to_zeitgeist(); + + Sibling::execute_with(|| { + assert_eq!(Tokens::free_balance(ETH_ID, &BOB), sibling_bob_initial_balance,); + }); + + Zeitgeist::execute_with(|| { + assert_ok!(XTokens::transfer( + RuntimeOrigin::signed(ALICE), + ETH_ID, + transfer_amount, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(PARA_ID_SIBLING), + Junction::AccountId32 { network: NetworkId::Any, id: BOB.into() } + ) + ) + .into() + ), + Limited(4_000_000_000), + )); + + // Confirm that Alice's balance is initial_balance - amount_transferred + assert_eq!(Tokens::free_balance(ETH_ID, &ALICE), 0); + }); + + Sibling::execute_with(|| { + let fee_adjusted = adjusted_balance(eth(1), eth_fee()); + let expected = transfer_amount - fee_adjusted; + + // Verify that Bob now has initial balance + amount transferred - fee + assert_eq!(Balances::free_balance(&BOB), sibling_bob_initial_balance + expected,); + + // Verify that the amount transferred is now subtracted from the zeitgeist account at sibling + assert_eq!( + Balances::free_balance(zeitgeist_parachain_account()), + initial_sovereign_balance - transfer_amount + ); + }); +} + #[test] fn transfer_dot_from_relay_chain() { TestNet::reset(); @@ -224,7 +460,7 @@ fn transfer_dot_to_relay_chain() { ) .into() ), - xcm_emulator::Limited(4_000_000_000) + Limited(4_000_000_000) )); assert_eq!( @@ -286,7 +522,7 @@ fn transfer_ztg_to_sibling_with_custom_fee() { ) .into() ), - xcm_emulator::Limited(4_000_000_000), + Limited(4_000_000_000), )); // Confirm that Alice's balance is initial_balance - amount_transferred @@ -337,7 +573,17 @@ fn dot_fee() -> Balance { } #[inline] -fn calc_fee(fee_per_second: Balance) -> Balance { +fn btc_fee() -> Balance { + fee(8) +} + +#[inline] +fn eth_fee() -> Balance { + fee(18) +} + +#[inline] +const fn calc_fee(fee_per_second: Balance) -> Balance { // We divide the fee to align its unit and multiply by 8 as that seems to be the unit of // time the tests take. // NOTE: it is possible that in different machines this value may differ. We shall see. diff --git a/runtime/zeitgeist/src/xcm_config/config.rs b/runtime/zeitgeist/src/xcm_config/config.rs index 888b97a3c..03a8e3d55 100644 --- a/runtime/zeitgeist/src/xcm_config/config.rs +++ b/runtime/zeitgeist/src/xcm_config/config.rs @@ -22,9 +22,14 @@ use crate::{ RuntimeOrigin, UnitWeightCost, UnknownTokens, XcmpQueue, ZeitgeistTreasuryAccount, }; -use frame_support::{parameter_types, traits::Everything, WeakBoundedVec}; +use core::marker::PhantomData; +use frame_support::{ + parameter_types, + traits::{ConstU8, Everything, Get}, + WeakBoundedVec, +}; use orml_asset_registry::{AssetRegistryTrader, FixedRateAssetRegistryTrader}; -use orml_traits::{location::AbsoluteReserveProvider, MultiCurrency}; +use orml_traits::{asset_registry::Inspect, location::AbsoluteReserveProvider, MultiCurrency}; use orml_xcm_support::{ DepositToAlternative, IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset, }; @@ -34,7 +39,7 @@ use sp_runtime::traits::Convert; use xcm::{ latest::{ prelude::{AccountId32, AssetId, Concrete, GeneralKey, MultiAsset, NetworkId, X1, X2}, - Junction, MultiLocation, + Error as XcmError, Junction, MultiLocation, Result as XcmResult, }, opaque::latest::Fungibility::Fungible, }; @@ -45,8 +50,8 @@ use xcm_builder::{ SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeRevenue, TakeWeightCredit, }; -use xcm_executor::Config; -use zeitgeist_primitives::types::Asset; +use xcm_executor::{traits::TransactAsset, Assets, Config}; +use zeitgeist_primitives::{constants::BalanceFractionalDecimals, types::Asset}; pub mod zeitgeist { #[cfg(test)] @@ -63,7 +68,7 @@ impl Config for XcmConfig { /// The handler for when there is an instruction to claim assets. type AssetClaims = PolkadotXcm; /// How to withdraw and deposit an asset. - type AssetTransactor = MultiAssetTransactor; + type AssetTransactor = AlignedFractionalMultiAssetTransactor; /// The general asset trap - handler for when assets are left in the Holding Register at the /// end of execution. type AssetTrap = PolkadotXcm; @@ -158,6 +163,108 @@ parameter_types! { ); } +/// A generic warpper around implementations of the (xcm-executor) `TransactAsset` trait. +/// +/// Aligns the fractional decimal places of every incoming token with ZTG. +/// Reconstructs the original number of fractional decimal places of every outgoing token. +/// +/// Important: Always use the global canonical representation of token balances in XCM. +/// Only during the interpretation of those XCM adjustments happens. +/// +/// Important: The implementation does not support teleports. +#[allow(clippy::type_complexity)] +pub struct AlignedFractionalTransactAsset< + AssetRegistry, + CurrencyIdConvert, + FracDecPlaces, + TransactAssetDelegate, +> { + _phantom: PhantomData<(AssetRegistry, CurrencyIdConvert, FracDecPlaces, TransactAssetDelegate)>, +} + +impl< + AssetRegistry: Inspect, + FracDecPlaces: Get, + CurrencyIdConvert: Convert>, + TransactAssetDelegate: TransactAsset, +> + AlignedFractionalTransactAsset< + AssetRegistry, + CurrencyIdConvert, + FracDecPlaces, + TransactAssetDelegate, + > +{ + fn adjust_fractional_places(asset: &MultiAsset) -> MultiAsset { + if let Some(ref asset_id) = CurrencyIdConvert::convert(asset.clone()) { + if let Fungible(amount) = asset.fun { + let mut asset_updated = asset.clone(); + let native_decimals = u32::from(FracDecPlaces::get()); + let metadata = AssetRegistry::metadata(asset_id); + + if let Some(metadata) = metadata { + let decimals = metadata.decimals; + + asset_updated.fun = if decimals > native_decimals { + let power = decimals.saturating_sub(native_decimals); + let adjust_factor = 10u128.saturating_pow(power); + // Floors the adjusted token amount, thus no tokens are generated + Fungible(amount.saturating_div(adjust_factor)) + } else { + let power = native_decimals.saturating_sub(decimals); + let adjust_factor = 10u128.saturating_pow(power); + Fungible(amount.saturating_mul(adjust_factor)) + }; + + return asset_updated; + } + } + } + + asset.clone() + } +} + +impl< + AssetRegistry: Inspect, + CurrencyIdConvert: Convert>, + FracDecPlaces: Get, + TransactAssetDelegate: TransactAsset, +> TransactAsset + for AlignedFractionalTransactAsset< + AssetRegistry, + CurrencyIdConvert, + FracDecPlaces, + TransactAssetDelegate, + > +{ + fn deposit_asset(asset: &MultiAsset, location: &MultiLocation) -> XcmResult { + let asset_adjusted = Self::adjust_fractional_places(asset); + TransactAssetDelegate::deposit_asset(&asset_adjusted, location) + } + + fn withdraw_asset(asset: &MultiAsset, location: &MultiLocation) -> Result { + let asset_adjusted = Self::adjust_fractional_places(asset); + TransactAssetDelegate::withdraw_asset(&asset_adjusted, location) + } + + fn transfer_asset( + asset: &MultiAsset, + from: &MultiLocation, + to: &MultiLocation, + ) -> Result { + let asset_adjusted = Self::adjust_fractional_places(asset); + TransactAssetDelegate::transfer_asset(&asset_adjusted, from, to) + } +} + +pub type AlignedFractionalMultiAssetTransactor = AlignedFractionalTransactAsset< + AssetRegistry, + AssetConvert, + ConstU8<{ BalanceFractionalDecimals::get() }>, + MultiAssetTransactor, +>; + /// Means for transacting assets on this chain. pub type MultiAssetTransactor = MultiCurrencyAdapter< // All known Assets will be processed by the following MultiCurrency implementation. diff --git a/runtime/zeitgeist/src/xcm_config/fees.rs b/runtime/zeitgeist/src/xcm_config/fees.rs index 53a036e53..df884ee7d 100644 --- a/runtime/zeitgeist/src/xcm_config/fees.rs +++ b/runtime/zeitgeist/src/xcm_config/fees.rs @@ -21,7 +21,7 @@ use core::marker::PhantomData; use frame_support::weights::constants::{ExtrinsicBaseWeight, WEIGHT_PER_SECOND}; use xcm::latest::MultiLocation; use zeitgeist_primitives::{constants::BalanceFractionalDecimals, types::CustomMetadata}; -use zrml_swaps::check_arithm_rslt::CheckArithmRslt; +use zrml_swaps::fixed::bmul; /// The fee cost per second for transferring the native token in cents. pub fn native_per_second() -> Balance { @@ -48,14 +48,9 @@ pub fn cent(decimals: u32) -> Balance { dollar(decimals).saturating_div(100) } -pub fn bmul(a: u128, b: u128, base: u128) -> Option { - let c0 = a.check_mul_rslt(&b).ok()?; - let c1 = c0.check_add_rslt(&base.check_div_rslt(&2).ok()?).ok()?; - c1.check_div_rslt(&base).ok() -} - -/// Our FixedConversionRateProvider, used to charge XCM-related fees for tokens registered in +/// The FixedConversionRateProvider is used charge XCM-related fees for tokens registered in /// the asset registry that were not already handled by native Trader rules. +/// Assumes that the fee factor is stored in the native base. pub struct FixedConversionRateProvider(PhantomData); impl< @@ -68,13 +63,22 @@ impl< { fn get_fee_per_second(location: &MultiLocation) -> Option { let metadata = AssetRegistry::metadata_by_location(location)?; - let default_per_second = default_per_second(metadata.decimals); + let default_per_second = native_per_second(); + let native_decimals: u32 = BalanceFractionalDecimals::get().into(); + let foreign_decimals = metadata.decimals; + + let fee_unadjusted = if let Some(fee_factor) = metadata.additional.xcm.fee_factor { + bmul(default_per_second, fee_factor).ok()? + } else { + default_per_second + }; - if let Some(fee_factor) = metadata.additional.xcm.fee_factor { - let base = 10u128.checked_pow(metadata.decimals)?; - bmul(default_per_second, fee_factor, base) + if native_decimals > foreign_decimals { + let power = native_decimals.saturating_sub(foreign_decimals); + Some(fee_unadjusted.checked_div(10u128.checked_pow(power)?)?) } else { - Some(default_per_second) + let power = foreign_decimals.saturating_sub(native_decimals); + Some(fee_unadjusted.checked_mul(10u128.checked_pow(power)?)?) } } }