mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-15 02:11:07 +00:00
XCM WeightTrader: Swap Fee Asset for Native Asset (#1845)
Implements an XCM executor `WeightTrader`, facilitating fee payments in any asset that can be exchanged for a native asset. A few constraints need to be observed: - `buy_weight` and `refund` operations must be atomic, as another weight trader implementation might be attempted in case of failure. - swap credit must be utilized since there isn’t an account to which an asset of some class can be deposited with a guarantee to meet the existential deposit requirement. Also, operating with credits enhances the efficiency of the weight trader - https://github.com/paritytech/polkadot-sdk/pull/1677 related PRs: - (depends) https://github.com/paritytech/polkadot-sdk/pull/2031 - (depends) https://github.com/paritytech/polkadot-sdk/pull/1677 - (caused) https://github.com/paritytech/polkadot-sdk/pull/1847 - (caused) https://github.com/paritytech/polkadot-sdk/pull/1876 // DONE: impl `OnUnbalanced` for a `fungible/s` credit // DONE: make the trader free from a concept of a native currency and drop few fallible conversions. related issue - https://github.com/paritytech/polkadot-sdk/issues/1842 // DONE: tests --------- Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>
This commit is contained in:
@@ -22,19 +22,24 @@
|
||||
use codec::Encode;
|
||||
use cumulus_primitives_core::{MessageSendError, UpwardMessageSender};
|
||||
use frame_support::{
|
||||
traits::{
|
||||
tokens::{fungibles, fungibles::Inspect},
|
||||
Get,
|
||||
},
|
||||
weights::Weight,
|
||||
defensive,
|
||||
traits::{tokens::fungibles, Get, OnUnbalanced as OnUnbalancedT},
|
||||
weights::{Weight, WeightToFee as WeightToFeeT},
|
||||
};
|
||||
use pallet_asset_conversion::SwapCredit as SwapCreditT;
|
||||
use polkadot_runtime_common::xcm_sender::PriceForMessageDelivery;
|
||||
use sp_runtime::{traits::Saturating, SaturatedConversion};
|
||||
use sp_runtime::{
|
||||
traits::{Saturating, Zero},
|
||||
SaturatedConversion,
|
||||
};
|
||||
use sp_std::{marker::PhantomData, prelude::*};
|
||||
use xcm::{latest::prelude::*, WrapVersion};
|
||||
use xcm_builder::TakeRevenue;
|
||||
use xcm_executor::traits::{MatchesFungibles, TransactAsset, WeightTrader};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Xcm router which recognises the `Parent` destination and handles it by sending the message into
|
||||
/// the given UMP `UpwardMessageSender` implementation. Thus this essentially adapts an
|
||||
/// `UpwardMessageSender` trait impl into a `SendXcm` trait impl.
|
||||
@@ -286,23 +291,238 @@ impl<
|
||||
/// in such assetId for that amount of weight
|
||||
pub trait ChargeWeightInFungibles<AccountId, Assets: fungibles::Inspect<AccountId>> {
|
||||
fn charge_weight_in_fungibles(
|
||||
asset_id: <Assets as Inspect<AccountId>>::AssetId,
|
||||
asset_id: <Assets as fungibles::Inspect<AccountId>>::AssetId,
|
||||
weight: Weight,
|
||||
) -> Result<<Assets as Inspect<AccountId>>::Balance, XcmError>;
|
||||
) -> Result<<Assets as fungibles::Inspect<AccountId>>::Balance, XcmError>;
|
||||
}
|
||||
|
||||
/// Provides an implementation of [`WeightTrader`] to charge for weight using the first asset
|
||||
/// specified in the `payment` argument.
|
||||
///
|
||||
/// The asset used to pay for the weight must differ from the `Target` asset and be exchangeable for
|
||||
/// the same `Target` asset through `SwapCredit`.
|
||||
///
|
||||
/// ### Parameters:
|
||||
/// - `Target`: the asset into which the user's payment will be exchanged using `SwapCredit`.
|
||||
/// - `SwapCredit`: mechanism used for the exchange of the user's payment asset into the `Target`.
|
||||
/// - `WeightToFee`: weight to the `Target` asset fee calculator.
|
||||
/// - `Fungibles`: registry of fungible assets.
|
||||
/// - `FungiblesAssetMatcher`: utility for mapping [`MultiAsset`] to `Fungibles::AssetId` and
|
||||
/// `Fungibles::Balance`.
|
||||
/// - `OnUnbalanced`: handler for the fee payment.
|
||||
/// - `AccountId`: the account identifier type.
|
||||
pub struct SwapFirstAssetTrader<
|
||||
Target: Get<Fungibles::AssetId>,
|
||||
SwapCredit: SwapCreditT<
|
||||
AccountId,
|
||||
Balance = Fungibles::Balance,
|
||||
AssetKind = Fungibles::AssetId,
|
||||
Credit = fungibles::Credit<AccountId, Fungibles>,
|
||||
>,
|
||||
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
|
||||
Fungibles: fungibles::Balanced<AccountId>,
|
||||
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
|
||||
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
|
||||
AccountId,
|
||||
> where
|
||||
Fungibles::Balance: Into<u128>,
|
||||
{
|
||||
/// Accumulated fee paid for XCM execution.
|
||||
total_fee: fungibles::Credit<AccountId, Fungibles>,
|
||||
/// Last asset utilized by a client to settle a fee.
|
||||
last_fee_asset: Option<AssetId>,
|
||||
_phantom_data: PhantomData<(
|
||||
Target,
|
||||
SwapCredit,
|
||||
WeightToFee,
|
||||
Fungibles,
|
||||
FungiblesAssetMatcher,
|
||||
OnUnbalanced,
|
||||
AccountId,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl<
|
||||
Target: Get<Fungibles::AssetId>,
|
||||
SwapCredit: SwapCreditT<
|
||||
AccountId,
|
||||
Balance = Fungibles::Balance,
|
||||
AssetKind = Fungibles::AssetId,
|
||||
Credit = fungibles::Credit<AccountId, Fungibles>,
|
||||
>,
|
||||
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
|
||||
Fungibles: fungibles::Balanced<AccountId>,
|
||||
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
|
||||
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
|
||||
AccountId,
|
||||
> WeightTrader
|
||||
for SwapFirstAssetTrader<
|
||||
Target,
|
||||
SwapCredit,
|
||||
WeightToFee,
|
||||
Fungibles,
|
||||
FungiblesAssetMatcher,
|
||||
OnUnbalanced,
|
||||
AccountId,
|
||||
> where
|
||||
Fungibles::Balance: Into<u128>,
|
||||
{
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
total_fee: fungibles::Credit::<AccountId, Fungibles>::zero(Target::get()),
|
||||
last_fee_asset: None,
|
||||
_phantom_data: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn buy_weight(
|
||||
&mut self,
|
||||
weight: Weight,
|
||||
mut payment: xcm_executor::Assets,
|
||||
_context: &XcmContext,
|
||||
) -> Result<xcm_executor::Assets, XcmError> {
|
||||
log::trace!(
|
||||
target: "xcm::weight",
|
||||
"SwapFirstAssetTrader::buy_weight weight: {:?}, payment: {:?}",
|
||||
weight,
|
||||
payment,
|
||||
);
|
||||
let first_asset: MultiAsset =
|
||||
payment.fungible.pop_first().ok_or(XcmError::AssetNotFound)?.into();
|
||||
let (fungibles_asset, balance) = FungiblesAssetMatcher::matches_fungibles(&first_asset)
|
||||
.map_err(|_| XcmError::AssetNotFound)?;
|
||||
|
||||
let swap_asset = fungibles_asset.clone().into();
|
||||
if Target::get().eq(&swap_asset) {
|
||||
// current trader is not applicable.
|
||||
return Err(XcmError::FeesNotMet)
|
||||
}
|
||||
|
||||
let credit_in = Fungibles::issue(fungibles_asset, balance);
|
||||
let fee = WeightToFee::weight_to_fee(&weight);
|
||||
|
||||
// swap the user's asset for the `Target` asset.
|
||||
let (credit_out, credit_change) = SwapCredit::swap_tokens_for_exact_tokens(
|
||||
vec![swap_asset, Target::get()],
|
||||
credit_in,
|
||||
fee,
|
||||
)
|
||||
.map_err(|(credit_in, _)| {
|
||||
drop(credit_in);
|
||||
XcmError::FeesNotMet
|
||||
})?;
|
||||
|
||||
match self.total_fee.subsume(credit_out) {
|
||||
Err(credit_out) => {
|
||||
// error may occur if `total_fee.asset` differs from `credit_out.asset`, which does
|
||||
// not apply in this context.
|
||||
defensive!(
|
||||
"`total_fee.asset` must be equal to `credit_out.asset`",
|
||||
(self.total_fee.asset(), credit_out.asset())
|
||||
);
|
||||
return Err(XcmError::FeesNotMet)
|
||||
},
|
||||
_ => (),
|
||||
};
|
||||
self.last_fee_asset = Some(first_asset.id);
|
||||
|
||||
payment.fungible.insert(first_asset.id, credit_change.peek().into());
|
||||
drop(credit_change);
|
||||
Ok(payment)
|
||||
}
|
||||
|
||||
fn refund_weight(&mut self, weight: Weight, _context: &XcmContext) -> Option<MultiAsset> {
|
||||
log::trace!(
|
||||
target: "xcm::weight",
|
||||
"SwapFirstAssetTrader::refund_weight weight: {:?}, self.total_fee: {:?}",
|
||||
weight,
|
||||
self.total_fee,
|
||||
);
|
||||
if self.total_fee.peek().is_zero() {
|
||||
// noting yet paid to refund.
|
||||
return None
|
||||
}
|
||||
let mut refund_asset = if let Some(asset) = &self.last_fee_asset {
|
||||
// create an initial zero refund in the asset used in the last `buy_weight`.
|
||||
(*asset, Fungible(0)).into()
|
||||
} else {
|
||||
return None
|
||||
};
|
||||
let refund_amount = WeightToFee::weight_to_fee(&weight);
|
||||
if refund_amount >= self.total_fee.peek() {
|
||||
// not enough was paid to refund the `weight`.
|
||||
return None
|
||||
}
|
||||
|
||||
let refund_swap_asset = FungiblesAssetMatcher::matches_fungibles(&refund_asset)
|
||||
.map(|(a, _)| a.into())
|
||||
.ok()?;
|
||||
|
||||
let refund = self.total_fee.extract(refund_amount);
|
||||
let refund = match SwapCredit::swap_exact_tokens_for_tokens(
|
||||
vec![Target::get(), refund_swap_asset],
|
||||
refund,
|
||||
None,
|
||||
) {
|
||||
Ok(refund_in_target) => refund_in_target,
|
||||
Err((refund, _)) => {
|
||||
// return an attempted refund back to the `total_fee`.
|
||||
let _ = self.total_fee.subsume(refund).map_err(|refund| {
|
||||
// error may occur if `total_fee.asset` differs from `refund.asset`, which does
|
||||
// not apply in this context.
|
||||
defensive!(
|
||||
"`total_fee.asset` must be equal to `refund.asset`",
|
||||
(self.total_fee.asset(), refund.asset())
|
||||
);
|
||||
});
|
||||
return None
|
||||
},
|
||||
};
|
||||
|
||||
refund_asset.fun = refund.peek().into().into();
|
||||
drop(refund);
|
||||
Some(refund_asset)
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
Target: Get<Fungibles::AssetId>,
|
||||
SwapCredit: SwapCreditT<
|
||||
AccountId,
|
||||
Balance = Fungibles::Balance,
|
||||
AssetKind = Fungibles::AssetId,
|
||||
Credit = fungibles::Credit<AccountId, Fungibles>,
|
||||
>,
|
||||
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
|
||||
Fungibles: fungibles::Balanced<AccountId>,
|
||||
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
|
||||
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
|
||||
AccountId,
|
||||
> Drop
|
||||
for SwapFirstAssetTrader<
|
||||
Target,
|
||||
SwapCredit,
|
||||
WeightToFee,
|
||||
Fungibles,
|
||||
FungiblesAssetMatcher,
|
||||
OnUnbalanced,
|
||||
AccountId,
|
||||
> where
|
||||
Fungibles::Balance: Into<u128>,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
if self.total_fee.peek().is_zero() {
|
||||
return
|
||||
}
|
||||
let total_fee = self.total_fee.extract(self.total_fee.peek());
|
||||
OnUnbalanced::on_unbalanced(total_fee);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod test_xcm_router {
|
||||
use super::*;
|
||||
use cumulus_primitives_core::UpwardMessage;
|
||||
use frame_support::{
|
||||
assert_ok,
|
||||
traits::tokens::{
|
||||
DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence,
|
||||
},
|
||||
};
|
||||
use sp_runtime::DispatchError;
|
||||
use xcm_executor::{traits::Error, Assets};
|
||||
|
||||
/// Validates [`validate`] for required Some(destination) and Some(message)
|
||||
struct OkFixedXcmHashWithAssertingRequiredInputsSender;
|
||||
@@ -398,6 +618,18 @@ mod tests {
|
||||
)>(dest.into(), message)
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test_trader {
|
||||
use super::*;
|
||||
use frame_support::{
|
||||
assert_ok,
|
||||
traits::tokens::{
|
||||
DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence,
|
||||
},
|
||||
};
|
||||
use sp_runtime::DispatchError;
|
||||
use xcm_executor::{traits::Error, Assets};
|
||||
|
||||
#[test]
|
||||
fn take_first_asset_trader_buy_weight_called_twice_throws_error() {
|
||||
@@ -491,9 +723,9 @@ mod tests {
|
||||
struct FeeChargerAssetsHandleRefund;
|
||||
impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund {
|
||||
fn charge_weight_in_fungibles(
|
||||
_: <TestAssets as Inspect<TestAccountId>>::AssetId,
|
||||
_: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
|
||||
_: Weight,
|
||||
) -> Result<<TestAssets as Inspect<TestAccountId>>::Balance, XcmError> {
|
||||
) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
|
||||
Ok(AMOUNT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Cumulus.
|
||||
|
||||
// Substrate 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.
|
||||
|
||||
// Substrate 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 Cumulus. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
mod swap_first;
|
||||
@@ -0,0 +1,551 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Cumulus.
|
||||
|
||||
// Substrate 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.
|
||||
|
||||
// Substrate 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 Cumulus. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::*;
|
||||
use frame_support::{parameter_types, traits::fungibles::Inspect};
|
||||
use mock::{setup_pool, AccountId, AssetId, Balance, Fungibles};
|
||||
use xcm::latest::AssetId as XcmAssetId;
|
||||
use xcm_executor::Assets as HoldingAsset;
|
||||
|
||||
fn create_holding_asset(asset_id: AssetId, amount: Balance) -> HoldingAsset {
|
||||
create_asset(asset_id, amount).into()
|
||||
}
|
||||
|
||||
fn create_asset(asset_id: AssetId, amount: Balance) -> MultiAsset {
|
||||
MultiAsset { id: create_asset_id(asset_id), fun: Fungible(amount) }
|
||||
}
|
||||
|
||||
fn create_asset_id(asset_id: AssetId) -> XcmAssetId {
|
||||
Concrete(MultiLocation::new(0, X1(GeneralIndex(asset_id.into()))))
|
||||
}
|
||||
|
||||
fn xcm_context() -> XcmContext {
|
||||
XcmContext { origin: None, message_id: [0u8; 32], topic: None }
|
||||
}
|
||||
|
||||
fn weight_worth_of(fee: Balance) -> Weight {
|
||||
Weight::from_parts(fee.try_into().unwrap(), 0)
|
||||
}
|
||||
|
||||
const TARGET_ASSET: AssetId = 1;
|
||||
const CLIENT_ASSET: AssetId = 2;
|
||||
const CLIENT_ASSET_2: AssetId = 3;
|
||||
|
||||
parameter_types! {
|
||||
pub const TargetAsset: AssetId = TARGET_ASSET;
|
||||
}
|
||||
|
||||
pub type Trader = SwapFirstAssetTrader<
|
||||
TargetAsset,
|
||||
mock::Swap,
|
||||
mock::WeightToFee,
|
||||
mock::Fungibles,
|
||||
mock::FungiblesMatcher,
|
||||
(),
|
||||
AccountId,
|
||||
>;
|
||||
|
||||
#[test]
|
||||
fn holding_asset_swap_for_target() {
|
||||
let client_asset_total = 15;
|
||||
let fee = 5;
|
||||
|
||||
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
|
||||
|
||||
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
|
||||
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
|
||||
|
||||
let target_total = Fungibles::total_issuance(TARGET_ASSET);
|
||||
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
|
||||
|
||||
let mut trader = Trader::new();
|
||||
assert_eq!(
|
||||
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
|
||||
holding_change
|
||||
);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
|
||||
|
||||
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
|
||||
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn holding_asset_swap_for_target_twice() {
|
||||
let client_asset_total = 20;
|
||||
let fee1 = 5;
|
||||
let fee2 = 6;
|
||||
|
||||
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
|
||||
|
||||
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
|
||||
let holding_change1 = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1);
|
||||
let holding_change2 = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1 - fee2);
|
||||
|
||||
let target_total = Fungibles::total_issuance(TARGET_ASSET);
|
||||
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
|
||||
|
||||
let mut trader = Trader::new();
|
||||
assert_eq!(
|
||||
trader.buy_weight(weight_worth_of(fee1), holding_asset, &xcm_context()).unwrap(),
|
||||
holding_change1
|
||||
);
|
||||
assert_eq!(
|
||||
trader
|
||||
.buy_weight(weight_worth_of(fee2), holding_change1, &xcm_context())
|
||||
.unwrap(),
|
||||
holding_change2
|
||||
);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee1 + fee2);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
|
||||
|
||||
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
|
||||
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee1 + fee2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_and_refund_twice_for_target() {
|
||||
let client_asset_total = 15;
|
||||
let fee = 5;
|
||||
let refund1 = 4;
|
||||
let refund2 = 2;
|
||||
|
||||
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
|
||||
// create pool for refund swap.
|
||||
setup_pool(TARGET_ASSET, 1000, CLIENT_ASSET, 1000);
|
||||
|
||||
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
|
||||
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
|
||||
let refund_asset = create_asset(CLIENT_ASSET, refund1);
|
||||
|
||||
let target_total = Fungibles::total_issuance(TARGET_ASSET);
|
||||
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
|
||||
|
||||
let mut trader = Trader::new();
|
||||
assert_eq!(
|
||||
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
|
||||
holding_change
|
||||
);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
|
||||
|
||||
assert_eq!(trader.refund_weight(weight_worth_of(refund1), &xcm_context()), Some(refund_asset));
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee - refund1);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
|
||||
|
||||
assert_eq!(trader.refund_weight(weight_worth_of(refund2), &xcm_context()), None);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee - refund1);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
|
||||
|
||||
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
|
||||
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee - refund1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_with_various_assets_and_refund_for_target() {
|
||||
let client_asset_total = 10;
|
||||
let client_asset_2_total = 15;
|
||||
let fee1 = 5;
|
||||
let fee2 = 6;
|
||||
let refund1 = 6;
|
||||
let refund2 = 4;
|
||||
|
||||
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
|
||||
setup_pool(CLIENT_ASSET_2, 1000, TARGET_ASSET, 1000);
|
||||
// create pool for refund swap.
|
||||
setup_pool(TARGET_ASSET, 1000, CLIENT_ASSET_2, 1000);
|
||||
|
||||
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
|
||||
let holding_asset_2 = create_holding_asset(CLIENT_ASSET_2, client_asset_2_total);
|
||||
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1);
|
||||
let holding_change_2 = create_holding_asset(CLIENT_ASSET_2, client_asset_2_total - fee2);
|
||||
// both refunds in the latest buy asset (`CLIENT_ASSET_2`).
|
||||
let refund_asset = create_asset(CLIENT_ASSET_2, refund1);
|
||||
let refund_asset_2 = create_asset(CLIENT_ASSET_2, refund2);
|
||||
|
||||
let target_total = Fungibles::total_issuance(TARGET_ASSET);
|
||||
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
|
||||
let client_total_2 = Fungibles::total_issuance(CLIENT_ASSET_2);
|
||||
|
||||
let mut trader = Trader::new();
|
||||
// first purchase with `CLIENT_ASSET`.
|
||||
assert_eq!(
|
||||
trader.buy_weight(weight_worth_of(fee1), holding_asset, &xcm_context()).unwrap(),
|
||||
holding_change
|
||||
);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee1);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
|
||||
|
||||
// second purchase with `CLIENT_ASSET_2`.
|
||||
assert_eq!(
|
||||
trader
|
||||
.buy_weight(weight_worth_of(fee2), holding_asset_2, &xcm_context())
|
||||
.unwrap(),
|
||||
holding_change_2
|
||||
);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee1 + fee2);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
|
||||
|
||||
// first refund in the last asset used with `buy_weight`.
|
||||
assert_eq!(trader.refund_weight(weight_worth_of(refund1), &xcm_context()), Some(refund_asset));
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee1 + fee2 - refund1);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
|
||||
|
||||
// second refund in the last asset used with `buy_weight`.
|
||||
assert_eq!(
|
||||
trader.refund_weight(weight_worth_of(refund2), &xcm_context()),
|
||||
Some(refund_asset_2)
|
||||
);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee1 + fee2 - refund1 - refund2);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
|
||||
|
||||
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
|
||||
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee1);
|
||||
assert_eq!(
|
||||
Fungibles::total_issuance(CLIENT_ASSET_2),
|
||||
client_total_2 + fee2 - refund1 - refund2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_enough_to_refund() {
|
||||
let client_asset_total = 15;
|
||||
let fee = 5;
|
||||
let refund = 6;
|
||||
|
||||
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
|
||||
|
||||
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
|
||||
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
|
||||
|
||||
let target_total = Fungibles::total_issuance(TARGET_ASSET);
|
||||
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
|
||||
|
||||
let mut trader = Trader::new();
|
||||
assert_eq!(
|
||||
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
|
||||
holding_change
|
||||
);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
|
||||
|
||||
assert_eq!(trader.refund_weight(weight_worth_of(refund), &xcm_context()), None);
|
||||
|
||||
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
|
||||
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_exchangeable_to_refund() {
|
||||
let client_asset_total = 15;
|
||||
let fee = 5;
|
||||
let refund = 1;
|
||||
|
||||
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
|
||||
|
||||
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
|
||||
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
|
||||
|
||||
let target_total = Fungibles::total_issuance(TARGET_ASSET);
|
||||
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
|
||||
|
||||
let mut trader = Trader::new();
|
||||
assert_eq!(
|
||||
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
|
||||
holding_change
|
||||
);
|
||||
|
||||
assert_eq!(trader.total_fee.peek(), fee);
|
||||
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
|
||||
|
||||
assert_eq!(trader.refund_weight(weight_worth_of(refund), &xcm_context()), None);
|
||||
|
||||
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
|
||||
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_to_refund() {
|
||||
let fee = 5;
|
||||
|
||||
let mut trader = Trader::new();
|
||||
assert_eq!(trader.refund_weight(weight_worth_of(fee), &xcm_context()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn holding_asset_not_exchangeable_for_target() {
|
||||
let holding_asset = create_holding_asset(CLIENT_ASSET, 10);
|
||||
|
||||
let target_total = Fungibles::total_issuance(TARGET_ASSET);
|
||||
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
|
||||
|
||||
let mut trader = Trader::new();
|
||||
assert_eq!(
|
||||
trader
|
||||
.buy_weight(Weight::from_all(10), holding_asset, &xcm_context())
|
||||
.unwrap_err(),
|
||||
XcmError::FeesNotMet
|
||||
);
|
||||
|
||||
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
|
||||
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_holding_asset() {
|
||||
let mut trader = Trader::new();
|
||||
assert_eq!(
|
||||
trader
|
||||
.buy_weight(Weight::from_all(10), HoldingAsset::new(), &xcm_context())
|
||||
.unwrap_err(),
|
||||
XcmError::AssetNotFound
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_to_match_holding_asset() {
|
||||
let mut trader = Trader::new();
|
||||
let holding_asset =
|
||||
MultiAsset { id: Concrete(MultiLocation::new(1, X1(Parachain(1)))), fun: Fungible(10) };
|
||||
assert_eq!(
|
||||
trader
|
||||
.buy_weight(Weight::from_all(10), holding_asset.into(), &xcm_context())
|
||||
.unwrap_err(),
|
||||
XcmError::AssetNotFound
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn holding_asset_equal_to_target_asset() {
|
||||
let mut trader = Trader::new();
|
||||
let holding_asset = create_holding_asset(TargetAsset::get(), 10);
|
||||
assert_eq!(
|
||||
trader
|
||||
.buy_weight(Weight::from_all(10), holding_asset, &xcm_context())
|
||||
.unwrap_err(),
|
||||
XcmError::FeesNotMet
|
||||
);
|
||||
}
|
||||
|
||||
pub mod mock {
|
||||
use crate::*;
|
||||
use core::cell::RefCell;
|
||||
use frame_support::{
|
||||
ensure,
|
||||
traits::{
|
||||
fungibles::{Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Unbalanced},
|
||||
tokens::{
|
||||
DepositConsequence, Fortitude, Fortitude::Polite, Precision::Exact, Preservation,
|
||||
Preservation::Preserve, Provenance, WithdrawConsequence,
|
||||
},
|
||||
},
|
||||
};
|
||||
use sp_runtime::{traits::One, DispatchError};
|
||||
use std::collections::HashMap;
|
||||
use xcm::latest::Junction;
|
||||
|
||||
pub type AccountId = u64;
|
||||
pub type AssetId = u32;
|
||||
pub type Balance = u128;
|
||||
pub type Credit = fungibles::Credit<AccountId, Fungibles>;
|
||||
|
||||
thread_local! {
|
||||
pub static TOTAL_ISSUANCE: RefCell<HashMap<AssetId, Balance>> = RefCell::new(HashMap::new());
|
||||
pub static ACCOUNT: RefCell<HashMap<(AssetId, AccountId), Balance>> = RefCell::new(HashMap::new());
|
||||
pub static SWAP: RefCell<HashMap<(AssetId, AssetId), AccountId>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub struct Swap {}
|
||||
impl SwapCreditT<AccountId> for Swap {
|
||||
type Balance = Balance;
|
||||
type AssetKind = AssetId;
|
||||
type Credit = Credit;
|
||||
fn max_path_len() -> u32 {
|
||||
2
|
||||
}
|
||||
fn swap_exact_tokens_for_tokens(
|
||||
path: Vec<Self::AssetKind>,
|
||||
credit_in: Self::Credit,
|
||||
amount_out_min: Option<Self::Balance>,
|
||||
) -> Result<Self::Credit, (Self::Credit, DispatchError)> {
|
||||
ensure!(2 == path.len(), (credit_in, DispatchError::Unavailable));
|
||||
ensure!(
|
||||
credit_in.peek() >= amount_out_min.unwrap_or(Self::Balance::zero()),
|
||||
(credit_in, DispatchError::Unavailable)
|
||||
);
|
||||
let swap_res = SWAP.with(|b| b.borrow().get(&(path[0], path[1])).map(|v| *v));
|
||||
let pool_account = match swap_res {
|
||||
Some(a) => a,
|
||||
None => return Err((credit_in, DispatchError::Unavailable)),
|
||||
};
|
||||
let credit_out = match Fungibles::withdraw(
|
||||
path[1],
|
||||
&pool_account,
|
||||
credit_in.peek(),
|
||||
Exact,
|
||||
Preserve,
|
||||
Polite,
|
||||
) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Err((credit_in, DispatchError::Unavailable)),
|
||||
};
|
||||
let _ = Fungibles::resolve(&pool_account, credit_in)
|
||||
.map_err(|c| (c, DispatchError::Unavailable))?;
|
||||
Ok(credit_out)
|
||||
}
|
||||
fn swap_tokens_for_exact_tokens(
|
||||
path: Vec<Self::AssetKind>,
|
||||
credit_in: Self::Credit,
|
||||
amount_out: Self::Balance,
|
||||
) -> Result<(Self::Credit, Self::Credit), (Self::Credit, DispatchError)> {
|
||||
ensure!(2 == path.len(), (credit_in, DispatchError::Unavailable));
|
||||
ensure!(credit_in.peek() >= amount_out, (credit_in, DispatchError::Unavailable));
|
||||
let swap_res = SWAP.with(|b| b.borrow().get(&(path[0], path[1])).map(|v| *v));
|
||||
let pool_account = match swap_res {
|
||||
Some(a) => a,
|
||||
None => return Err((credit_in, DispatchError::Unavailable)),
|
||||
};
|
||||
let credit_out = match Fungibles::withdraw(
|
||||
path[1],
|
||||
&pool_account,
|
||||
amount_out,
|
||||
Exact,
|
||||
Preserve,
|
||||
Polite,
|
||||
) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Err((credit_in, DispatchError::Unavailable)),
|
||||
};
|
||||
let (credit_in, change) = credit_in.split(amount_out);
|
||||
let _ = Fungibles::resolve(&pool_account, credit_in)
|
||||
.map_err(|c| (c, DispatchError::Unavailable))?;
|
||||
Ok((credit_out, change))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pool_account(asset1: AssetId, asset2: AssetId) -> AccountId {
|
||||
(1000 + asset1 * 10 + asset2 * 100).into()
|
||||
}
|
||||
|
||||
pub fn setup_pool(asset1: AssetId, liquidity1: Balance, asset2: AssetId, liquidity2: Balance) {
|
||||
let account = pool_account(asset1, asset2);
|
||||
SWAP.with(|b| b.borrow_mut().insert((asset1, asset2), account));
|
||||
let debt1 = Fungibles::deposit(asset1, &account, liquidity1, Exact);
|
||||
let debt2 = Fungibles::deposit(asset2, &account, liquidity2, Exact);
|
||||
drop(debt1);
|
||||
drop(debt2);
|
||||
}
|
||||
|
||||
pub struct WeightToFee;
|
||||
impl WeightToFeeT for WeightToFee {
|
||||
type Balance = Balance;
|
||||
fn weight_to_fee(weight: &Weight) -> Self::Balance {
|
||||
(weight.ref_time() + weight.proof_size()).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Fungibles {}
|
||||
impl Inspect<AccountId> for Fungibles {
|
||||
type AssetId = AssetId;
|
||||
type Balance = Balance;
|
||||
fn total_issuance(asset: Self::AssetId) -> Self::Balance {
|
||||
TOTAL_ISSUANCE.with(|b| b.borrow().get(&asset).map_or(Self::Balance::zero(), |b| *b))
|
||||
}
|
||||
fn minimum_balance(_: Self::AssetId) -> Self::Balance {
|
||||
Self::Balance::one()
|
||||
}
|
||||
fn total_balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance {
|
||||
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
|
||||
}
|
||||
fn balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance {
|
||||
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
|
||||
}
|
||||
fn reducible_balance(
|
||||
asset: Self::AssetId,
|
||||
who: &AccountId,
|
||||
_: Preservation,
|
||||
_: Fortitude,
|
||||
) -> Self::Balance {
|
||||
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
|
||||
}
|
||||
fn can_deposit(
|
||||
_: Self::AssetId,
|
||||
_: &AccountId,
|
||||
_: Self::Balance,
|
||||
_: Provenance,
|
||||
) -> DepositConsequence {
|
||||
unimplemented!()
|
||||
}
|
||||
fn can_withdraw(
|
||||
_: Self::AssetId,
|
||||
_: &AccountId,
|
||||
_: Self::Balance,
|
||||
) -> WithdrawConsequence<Self::Balance> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn asset_exists(_: Self::AssetId) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Unbalanced<AccountId> for Fungibles {
|
||||
fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) {
|
||||
TOTAL_ISSUANCE.with(|b| b.borrow_mut().insert(asset, amount));
|
||||
}
|
||||
fn handle_dust(_: Dust<AccountId, Self>) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn write_balance(
|
||||
asset: Self::AssetId,
|
||||
who: &AccountId,
|
||||
amount: Self::Balance,
|
||||
) -> Result<Option<Self::Balance>, DispatchError> {
|
||||
let _ = ACCOUNT.with(|b| b.borrow_mut().insert((asset, *who), amount));
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Balanced<AccountId> for Fungibles {
|
||||
type OnDropCredit = DecreaseIssuance<AccountId, Self>;
|
||||
type OnDropDebt = IncreaseIssuance<AccountId, Self>;
|
||||
}
|
||||
|
||||
pub struct FungiblesMatcher;
|
||||
impl MatchesFungibles<AssetId, Balance> for FungiblesMatcher {
|
||||
fn matches_fungibles(
|
||||
a: &MultiAsset,
|
||||
) -> core::result::Result<(AssetId, Balance), xcm_executor::traits::Error> {
|
||||
match a {
|
||||
MultiAsset {
|
||||
fun: Fungible(amount),
|
||||
id:
|
||||
Concrete(MultiLocation { parents: 0, interior: X1(Junction::GeneralIndex(id)) }),
|
||||
} => Ok(((*id).try_into().unwrap(), *amount)),
|
||||
_ => Err(xcm_executor::traits::Error::AssetNotHandled),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user