diff --git a/frame/asset-conversion/src/lib.rs b/frame/asset-conversion/src/lib.rs index 68a0210b40058..a2cce8278248b 100644 --- a/frame/asset-conversion/src/lib.rs +++ b/frame/asset-conversion/src/lib.rs @@ -78,11 +78,10 @@ use sp_arithmetic::traits::Unsigned; use sp_runtime::{ traits::{ CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Ensure, MaybeDisplay, TrailingZeroInput, - Zero, }, DispatchError, }; -use sp_std::vec; +use sp_std::prelude::*; pub use types::*; pub use weights::WeightInfo; @@ -108,7 +107,6 @@ pub mod pallet { traits::{IntegerSquareRoot, One, Zero}, Saturating, }; - use sp_std::prelude::*; #[pallet::pallet] pub struct Pallet(_); @@ -145,7 +143,7 @@ pub mod pallet { type AssetId: AssetId + PartialOrd; /// Type that identifies either the native currency or a token class from `Assets`. - type MultiAssetId: AssetId + Ord; + type MultiAssetId: AssetId + Ord + From; /// Type to convert an `AssetId` into `MultiAssetId`. type MultiAssetIdConverter: MultiAssetIdConverter; @@ -641,18 +639,14 @@ pub mod pallet { keep_alive: bool, ) -> DispatchResult { let sender = ensure_signed(origin)?; - - ensure!( - amount_in > Zero::zero() && amount_out_min > Zero::zero(), - Error::::ZeroAmount - ); - Self::validate_swap_path(&path)?; - - let amounts = Self::get_amounts_out(&amount_in, &path)?; - let amount_out = *amounts.last().expect("Has always more than 1 element"); - ensure!(amount_out >= amount_out_min, Error::::ProvidedMinimumNotSufficientForSwap); - - Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; + Self::do_swap_exact_tokens_for_tokens( + sender, + path, + amount_in, + Some(amount_out_min), + send_to, + keep_alive, + )?; Ok(()) } @@ -673,23 +667,95 @@ pub mod pallet { keep_alive: bool, ) -> DispatchResult { let sender = ensure_signed(origin)?; + Self::do_swap_tokens_for_exact_tokens( + sender, + path, + amount_out, + Some(amount_in_max), + send_to, + keep_alive, + )?; + Ok(()) + } + } + + impl Pallet { + /// Swap exactly `amount_in` of asset `path[0]` for asset `path[1]`. + /// If an `amount_out_min` is specified, it will return an error if it is unable to acquire + /// the amount desired. + /// + /// Withdraws the `path[0]` asset from `sender`, deposits the `path[1]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful, returns the amount of `path[1]` acquired for the `amount_in`. + pub fn do_swap_exact_tokens_for_tokens( + sender: T::AccountId, + path: BoundedVec, + amount_in: T::AssetBalance, + amount_out_min: Option, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + ensure!(amount_in > Zero::zero(), Error::::ZeroAmount); + if let Some(amount_out_min) = amount_out_min { + ensure!(amount_out_min > Zero::zero(), Error::::ZeroAmount); + } + + Self::validate_swap_path(&path)?; + + let amounts = Self::get_amounts_out(&amount_in, &path)?; + let amount_out = + *amounts.last().defensive_ok_or("get_amounts_out() returned an empty result")?; + + if let Some(amount_out_min) = amount_out_min { + ensure!( + amount_out >= amount_out_min, + Error::::ProvidedMinimumNotSufficientForSwap + ); + } + + Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; + Ok(amount_out) + } + + /// Take the `path[0]` asset and swap some amount for `amount_out` of the `path[1]`. If an + /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be + /// too costly. + /// + /// Withdraws `path[0]` asset from `sender`, deposits the `path[1]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. + pub fn do_swap_tokens_for_exact_tokens( + sender: T::AccountId, + path: BoundedVec, + amount_out: T::AssetBalance, + amount_in_max: Option, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + ensure!(amount_out > Zero::zero(), Error::::ZeroAmount); + if let Some(amount_in_max) = amount_in_max { + ensure!(amount_in_max > Zero::zero(), Error::::ZeroAmount); + } - ensure!( - amount_out > Zero::zero() && amount_in_max > Zero::zero(), - Error::::ZeroAmount - ); Self::validate_swap_path(&path)?; let amounts = Self::get_amounts_in(&amount_out, &path)?; - let amount_in = *amounts.first().expect("Always has more than one element"); - ensure!(amount_in <= amount_in_max, Error::::ProvidedMaximumNotSufficientForSwap); + let amount_in = + *amounts.first().defensive_ok_or("get_amounts_in() returned an empty result")?; + + if let Some(amount_in_max) = amount_in_max { + ensure!( + amount_in <= amount_in_max, + Error::::ProvidedMaximumNotSufficientForSwap + ); + } Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; - Ok(()) + Ok(amount_in) } - } - impl Pallet { /// Transfer an `amount` of `asset_id`, respecting the `keep_alive` requirements. fn transfer( asset_id: &T::MultiAssetId, @@ -746,6 +812,13 @@ pub mod pallet { .map_err(|_| Error::::Overflow) } + /// Convert a `HigherPrecisionBalance` type to an `AssetBalance`. + pub(crate) fn convert_hpb_to_asset_balance( + amount: T::HigherPrecisionBalance, + ) -> Result> { + amount.try_into().map_err(|_| Error::::Overflow) + } + /// Swap assets along a `path`, depositing in `send_to`. pub(crate) fn do_swap( sender: T::AccountId, @@ -1120,92 +1193,47 @@ pub mod pallet { } } -impl - frame_support::traits::tokens::fungibles::SwapNative< - T::RuntimeOrigin, - T::AccountId, - T::Balance, - T::AssetBalance, - T::AssetId, - > for Pallet -where - ::Currency: - frame_support::traits::tokens::fungible::Inspect<::AccountId>, -{ - /// Take an `asset_id` and swap some amount for `amount_out` of the chain's native asset. If an - /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be - /// too costly. - /// - /// If successful returns the amount of the `asset_id` taken to provide `amount_out`. - fn swap_tokens_for_exact_native( +impl Swap for Pallet { + fn swap_exact_tokens_for_tokens( sender: T::AccountId, - asset_id: T::AssetId, - amount_out: T::Balance, - amount_in_max: Option, + path: Vec, + amount_in: T::HigherPrecisionBalance, + amount_out_min: Option, send_to: T::AccountId, keep_alive: bool, - ) -> Result { - ensure!(amount_out > Zero::zero(), Error::::ZeroAmount); - if let Some(amount_in_max) = amount_in_max { - ensure!(amount_in_max > Zero::zero(), Error::::ZeroAmount); - } - let mut path = sp_std::vec::Vec::new(); - path.push(T::MultiAssetIdConverter::into_multiasset_id(&asset_id)); - path.push(T::MultiAssetIdConverter::get_native()); - let path = path.try_into().unwrap(); - - // convert `amount_out` from native balance type, to asset balance type - let amount_out = Self::convert_native_balance_to_asset_balance(amount_out)?; - - // calculate the amount we need to provide - let amounts = Self::get_amounts_in(&amount_out, &path)?; - let amount_in = - *amounts.first().defensive_ok_or("get_amounts_in() returned an empty result")?; - if let Some(amount_in_max) = amount_in_max { - ensure!(amount_in <= amount_in_max, Error::::ProvidedMaximumNotSufficientForSwap); - } - - Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; - Ok(amount_in) + ) -> Result { + let path = path.try_into().map_err(|_| Error::::PathError)?; + let amount_out_min = amount_out_min.map(Self::convert_hpb_to_asset_balance).transpose()?; + let amount_out = Self::do_swap_exact_tokens_for_tokens( + sender, + path, + Self::convert_hpb_to_asset_balance(amount_in)?, + amount_out_min, + send_to, + keep_alive, + )?; + Ok(amount_out.into()) } - /// Take an `asset_id` and swap `amount_in` of the chain's native asset for it. If an - /// `amount_out_min` is specified, it will return an error if it is unable to acquire the amount - /// desired. - /// - /// If successful, returns the amount of `asset_id` acquired for the `amount_in`. - fn swap_exact_native_for_tokens( + fn swap_tokens_for_exact_tokens( sender: T::AccountId, - asset_id: T::AssetId, - amount_in: T::Balance, - amount_out_min: Option, + path: Vec, + amount_out: T::HigherPrecisionBalance, + amount_in_max: Option, send_to: T::AccountId, keep_alive: bool, - ) -> Result { - ensure!(amount_in > Zero::zero(), Error::::ZeroAmount); - if let Some(amount_out_min) = amount_out_min { - ensure!(amount_out_min > Zero::zero(), Error::::ZeroAmount); - } - let mut path = sp_std::vec::Vec::new(); - path.push(T::MultiAssetIdConverter::get_native()); - path.push(T::MultiAssetIdConverter::into_multiasset_id(&asset_id)); - let path = path.try_into().expect( - "`MaxSwapPathLength` is ensured by to be greater than 2; pushed only twice; qed", - ); - - // convert `amount_in` from native balance type, to asset balance type - let amount_in = Self::convert_native_balance_to_asset_balance(amount_in)?; - - // calculate the amount we should receive - let amounts = Self::get_amounts_out(&amount_in, &path)?; - let amount_out = - *amounts.last().defensive_ok_or("get_amounts_out() returned an empty result")?; - if let Some(amount_out_min) = amount_out_min { - ensure!(amount_out >= amount_out_min, Error::::ProvidedMaximumNotSufficientForSwap); - } - - Self::do_swap(sender, &amounts, path, send_to, keep_alive)?; - Ok(amount_out) + ) -> Result { + let path = path.try_into().map_err(|_| Error::::PathError)?; + let amount_in_max = amount_in_max.map(Self::convert_hpb_to_asset_balance).transpose()?; + let amount_in = Self::do_swap_tokens_for_exact_tokens( + sender, + path, + Self::convert_hpb_to_asset_balance(amount_out)?, + amount_in_max, + send_to, + keep_alive, + )?; + Ok(amount_in.into()) } } diff --git a/frame/asset-conversion/src/types.rs b/frame/asset-conversion/src/types.rs index cbe201f01c10b..0612e363f0560 100644 --- a/frame/asset-conversion/src/types.rs +++ b/frame/asset-conversion/src/types.rs @@ -33,7 +33,7 @@ pub struct PoolInfo { /// A trait that converts between a MultiAssetId and either the native currency or an AssetId. pub trait MultiAssetIdConverter { - /// Returns the MultiAssetId reperesenting the native currency of the chain. + /// Returns the MultiAssetId representing the native currency of the chain. fn get_native() -> MultiAssetId; /// Returns true if the given MultiAssetId is the native currency. @@ -42,7 +42,7 @@ pub trait MultiAssetIdConverter { /// If it's not native, returns the AssetId for the given MultiAssetId. fn try_convert(asset: &MultiAssetId) -> Result; - /// Wrapps an AssetId as a MultiAssetId. + /// Wraps an AssetId as a MultiAssetId. fn into_multiasset_id(asset: &AssetId) -> MultiAssetId; } @@ -76,6 +76,12 @@ where Asset(AssetId), } +impl From for NativeOrAssetId { + fn from(asset: AssetId) -> Self { + Self::Asset(asset) + } +} + impl Ord for NativeOrAssetId { fn cmp(&self, other: &Self) -> Ordering { match (self, other) { @@ -126,3 +132,40 @@ impl MultiAssetIdConverter, Asset NativeOrAssetId::Asset((*asset).clone()) } } + +/// Trait for providing methods to swap between the various asset classes. +pub trait Swap { + /// Swap exactly `amount_in` of asset `path[0]` for asset `path[1]`. + /// If an `amount_out_min` is specified, it will return an error if it is unable to acquire + /// the amount desired. + /// + /// Withdraws the `path[0]` asset from `sender`, deposits the `path[1]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful, returns the amount of `path[1]` acquired for the `amount_in`. + fn swap_exact_tokens_for_tokens( + sender: AccountId, + path: Vec, + amount_in: Balance, + amount_out_min: Option, + send_to: AccountId, + keep_alive: bool, + ) -> Result; + + /// Take the `path[0]` asset and swap some amount for `amount_out` of the `path[1]`. If an + /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be + /// too costly. + /// + /// Withdraws `path[0]` asset from `sender`, deposits `path[1]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. + fn swap_tokens_for_exact_tokens( + sender: AccountId, + path: Vec, + amount_out: Balance, + amount_in_max: Option, + send_to: AccountId, + keep_alive: bool, + ) -> Result; +} diff --git a/frame/support/src/traits/tokens/fungibles/mod.rs b/frame/support/src/traits/tokens/fungibles/mod.rs index 8070d916fb330..697eff39ff748 100644 --- a/frame/support/src/traits/tokens/fungibles/mod.rs +++ b/frame/support/src/traits/tokens/fungibles/mod.rs @@ -36,5 +36,5 @@ pub use hold::{ pub use imbalance::{Credit, Debt, HandleImbalanceDrop, Imbalance}; pub use lifetime::{Create, Destroy}; pub use regular::{ - Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, SwapNative, Unbalanced, + Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, Unbalanced, }; diff --git a/frame/support/src/traits/tokens/fungibles/regular.rs b/frame/support/src/traits/tokens/fungibles/regular.rs index 3df0a6b6d3def..5570659e8115a 100644 --- a/frame/support/src/traits/tokens/fungibles/regular.rs +++ b/frame/support/src/traits/tokens/fungibles/regular.rs @@ -583,40 +583,3 @@ pub trait Balanced: Inspect + Unbalanced { fn done_deposit(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {} fn done_withdraw(_asset: Self::AssetId, _who: &AccountId, _amount: Self::Balance) {} } - -/// Trait for providing methods to swap between the chain's native token and other asset classes. -pub trait SwapNative { - /// Take an `asset_id` and swap some amount for `amount_out` of the chain's native asset. If an - /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be - /// too costly. - /// - /// Withdraws `asset_id` from `sender`, deposits native asset to `send_to`, respecting - /// `keep_alive`. - /// - /// If successful returns the amount of the `asset_id` taken to provide `amount_out`. - fn swap_tokens_for_exact_native( - sender: AccountId, - asset_id: AssetId, - amount_out: Balance, - amount_in_max: Option, - send_to: AccountId, - keep_alive: bool, - ) -> Result; - - /// Take an `asset_id` and swap `amount_in` of the chain's native asset for it. If an - /// `amount_out_min` is specified, it will return an error if it is unable to acquire the amount - /// desired. - /// - /// Withdraws native asset from `sender`, deposits `asset_id` to `send_to`, respecting - /// `keep_alive`. - /// - /// If successful, returns the amount of `asset_id` acquired for the `amount_in`. - fn swap_exact_native_for_tokens( - sender: AccountId, - asset_id: AssetId, - amount_in: Balance, - amount_out_min: Option, - send_to: AccountId, - keep_alive: bool, - ) -> Result; -} diff --git a/frame/transaction-payment/asset-conversion-tx-payment/Cargo.toml b/frame/transaction-payment/asset-conversion-tx-payment/Cargo.toml index 8f9e1af04a76a..1555f4d91365d 100644 --- a/frame/transaction-payment/asset-conversion-tx-payment/Cargo.toml +++ b/frame/transaction-payment/asset-conversion-tx-payment/Cargo.toml @@ -18,6 +18,7 @@ sp-runtime = { version = "24.0.0", default-features = false, path = "../../../pr sp-std = { version = "8.0.0", default-features = false, path = "../../../primitives/std" } frame-support = { version = "4.0.0-dev", default-features = false, path = "../../support" } frame-system = { version = "4.0.0-dev", default-features = false, path = "../../system" } +pallet-asset-conversion = { version = "4.0.0-dev", default-features = false, path = "../../asset-conversion" } pallet-transaction-payment = { version = "4.0.0-dev", default-features = false, path = ".." } codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } @@ -28,7 +29,6 @@ sp-io = { version = "23.0.0", default-features = false, path = "../../../primiti sp-storage = { version = "13.0.0", default-features = false, path = "../../../primitives/storage" } pallet-assets = { version = "4.0.0-dev", path = "../../assets" } pallet-balances = { version = "4.0.0-dev", path = "../../balances" } -pallet-asset-conversion = { version = "4.0.0-dev", path = "../../asset-conversion" } [features] default = ["std"] @@ -41,6 +41,7 @@ std = [ "frame-system/std", "sp-io/std", "sp-core/std", + "pallet-asset-conversion/std", "pallet-transaction-payment/std", ] try-runtime = ["frame-support/try-runtime"] diff --git a/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs b/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs index 7f6da90dfe95f..efc7386ae8c00 100644 --- a/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs +++ b/frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs @@ -69,6 +69,8 @@ mod mock; mod tests; mod payment; +use frame_support::traits::tokens::AssetId; +use pallet_asset_conversion::MultiAssetIdConverter; pub use payment::*; /// Type aliases used for interaction with `OnChargeTransaction`. @@ -116,7 +118,9 @@ pub mod pallet { use super::*; #[pallet::config] - pub trait Config: frame_system::Config + pallet_transaction_payment::Config { + pub trait Config: + frame_system::Config + pallet_transaction_payment::Config + pallet_asset_conversion::Config + { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The fungibles instance used to pay for transactions in assets. @@ -187,12 +191,12 @@ where debug_assert!(self.tip <= fee, "tip should be included in the computed fee"); if fee.is_zero() { Ok((fee, InitialPayment::Nothing)) - } else if let Some(asset_id) = self.asset_id { + } else if let Some(asset_id) = &self.asset_id { T::OnChargeAssetTransaction::withdraw_fee( who, call, info, - asset_id, + asset_id.clone(), fee.into(), self.tip.into(), ) @@ -324,7 +328,7 @@ where tip.into(), used_for_fee.into(), received_exchanged.into(), - asset_id, + asset_id.clone(), asset_consumed.into(), )?; diff --git a/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs b/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs index 86b2c09e541ac..0d090211d0352 100644 --- a/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs +++ b/frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs @@ -17,26 +17,25 @@ use super::*; use crate::Config; -use codec::FullCodec; use frame_support::{ ensure, - traits::{fungible::Inspect, fungibles::SwapNative, tokens::Balance}, + traits::{fungible::Inspect, tokens::Balance}, unsigned::TransactionValidityError, }; -use scale_info::TypeInfo; +use pallet_asset_conversion::Swap; use sp_runtime::{ - traits::{DispatchInfoOf, MaybeSerializeDeserialize, PostDispatchInfoOf, Zero}, + traits::{DispatchInfoOf, PostDispatchInfoOf, Zero}, transaction_validity::InvalidTransaction, Saturating, }; -use sp_std::{fmt::Debug, marker::PhantomData}; +use sp_std::marker::PhantomData; /// Handle withdrawing, refunding and depositing of transaction fees. pub trait OnChargeAssetTransaction { /// The underlying integer type in which fees are calculated. type Balance: Balance; /// The type used to identify the assets used for transaction payment. - type AssetId: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo; + type AssetId: AssetId; /// The type used to store the intermediate values between pre- and post-dispatch. type LiquidityInfo; @@ -74,8 +73,7 @@ pub trait OnChargeAssetTransaction { ) -> Result, TransactionValidityError>; } -/// Implements the asset transaction for a balance to asset converter (implementing -/// [`SwapNative`]). +/// Implements the asset transaction for a balance to asset converter (implementing [`Swap`]). /// /// The converter is given the complete fee in terms of the asset used for the transaction. pub struct AssetConversionAdapter(PhantomData<(C, CON)>); @@ -85,8 +83,9 @@ impl OnChargeAssetTransaction for AssetConversionAdapter where T: Config, C: Inspect<::AccountId>, - CON: SwapNative, AssetBalanceOf, AssetIdOf>, - AssetIdOf: FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default + Eq + TypeInfo, + CON: Swap, + T::HigherPrecisionBalance: From> + TryInto>, + T::MultiAssetId: From>, BalanceOf: IsType<::AccountId>>::Balance>, { type Balance = BalanceOf; @@ -115,16 +114,20 @@ where let native_asset_required = if C::balance(&who) >= ed.saturating_add(fee.into()) { fee } else { fee + ed.into() }; - let asset_consumed = CON::swap_tokens_for_exact_native( + let asset_consumed = CON::swap_tokens_for_exact_tokens( who.clone(), - asset_id, - native_asset_required, + vec![asset_id.into(), T::MultiAssetIdConverter::get_native()], + T::HigherPrecisionBalance::from(native_asset_required), None, who.clone(), true, ) .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))?; + let asset_consumed = asset_consumed + .try_into() + .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))?; + ensure!(asset_consumed > Zero::zero(), InvalidTransaction::Payment); // charge the fee in native currency @@ -166,17 +169,25 @@ where // If this fails, the account might have dropped below the existential balance or there // is not enough liquidity left in the pool. In that case we don't throw an error and // the account will keep the native currency. - match CON::swap_exact_native_for_tokens( + match CON::swap_exact_tokens_for_tokens( who.clone(), // we already deposited the native to `who` - asset_id, // we want asset_id back - swap_back, // amount of the native asset to convert to `asset_id` + vec![ + T::MultiAssetIdConverter::get_native(), // we provide the native + asset_id.into(), // we want asset_id back + ], + T::HigherPrecisionBalance::from(swap_back), /* amount of the native asset to + * convert to `asset_id` */ None, // no minimum amount back who.clone(), // we will refund to `who` false, // no need to keep alive ) .ok() { - Some(acquired) => asset_refund = acquired, + Some(acquired) => { + asset_refund = acquired + .try_into() + .map_err(|_| TransactionValidityError::from(InvalidTransaction::Payment))?; + }, None => { Pallet::::deposit_event(Event::::AssetRefundFailed { native_amount_kept: swap_back,