Pay tx fee with assets by using the asset conversion pallet (#14340)

* Pay tx by swapping the assets

* Change liquidity structure

* Uncomment the event

* Update frame/transaction-payment/asset-tx-payment/src/payment.rs

Co-authored-by: Squirrel <gilescope@gmail.com>

* New approach

* Fix bounds

* Clearer version

* Change IsType with Into and From

* Enable event

* Check ED + fix the logic

* Add temp comments

* Rework the refund

* Clean up

* Improve readability

* Getting closer

* fix

* Use fungible instead of Currency

* Test account without ed

* Final push

* Fixed

* Rename to pallet-asset-conversion-tx-payment

* Bring back the old pallet

* Update versions

* Update docs

* Update readme

* Wrong readme updated

* Revert back doc change

* Fix import

* Fix kitchensink

* Fix

* One more time..

* Wait pls

* Update frame/asset-conversion/src/lib.rs

Co-authored-by: Squirrel <gilescope@gmail.com>

* Update frame/support/src/traits/tokens/fungibles/regular.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update docs/comments

* Docs improvement

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/transaction-payment/asset-conversion-tx-payment/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Payed -> paid

* Docs

* Update frame/transaction-payment/asset-conversion-tx-payment/README.md

Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com>

* Rewrite docs

* Try to clean the deps

* Add debug assert

* Return back frame-benchmarking

* Update cargo

* Update frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Rename

* clearer error message

* Docs for Pay by Swap (#14445)

* docs

* better error name

* more comments

* more docs on swap trait

* Fix compile errors

* Another fix

* Refactoring

* Update frame/transaction-payment/asset-conversion-tx-payment/src/payment.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Emit an error if we fail to swap the refund back

* Add integrity_test

* Update frame/asset-conversion/src/lib.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* Fmt

* Use defensive_ok_or

* child PR: Tidy swap event (#14441)

* Dedup raising swap event

* use expect rather than unwrap

* Additional checks for future defence.

* cargo fmt

* Update frame/asset-conversion/src/lib.rs

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

---------

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

---------

Co-authored-by: Squirrel <gilescope@gmail.com>
Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Jegor Sidorenko
2023-06-23 21:17:52 +02:00
committed by GitHub
parent 3e2c73dfad
commit be7c654c42
21 changed files with 1803 additions and 85 deletions
+124 -56
View File
@@ -53,7 +53,7 @@
//! (This can be run against the kitchen sync node in the `node` folder of this repo.)
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::traits::Incrementable;
use frame_support::traits::{DefensiveOption, Incrementable};
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
@@ -307,7 +307,10 @@ pub mod pallet {
WrongDesiredAmount,
/// Provided amount should be greater than or equal to the existential deposit/asset's
/// minimal amount.
AmountLessThanMinimal,
AmountOneLessThanMinimal,
/// Provided amount should be greater than or equal to the existential deposit/asset's
/// minimal amount.
AmountTwoLessThanMinimal,
/// Reserve needs to always be greater than or equal to the existential deposit/asset's
/// minimal amount.
ReserveLeftLessThanMinimal,
@@ -347,6 +350,20 @@ pub mod pallet {
PathError,
/// The provided path must consists of unique assets.
NonUniquePath,
/// Unable to find an element in an array/vec that should have one-to-one correspondence
/// with another. For example, an array of assets constituting a `path` should have a
/// corresponding array of `amounts` along the path.
CorrespondenceError,
}
#[pallet::hooks]
impl<T: Config> Hooks<T::BlockNumber> for Pallet<T> {
fn integrity_test() {
assert!(
T::MaxSwapPathLength::get() > 1,
"the `MaxSwapPathLength` should be greater than 1",
);
}
}
/// Pallet's callable functions.
@@ -488,9 +505,9 @@ pub mod pallet {
}
Self::validate_minimal_amount(amount1.saturating_add(reserve1), &asset1)
.map_err(|_| Error::<T>::AmountLessThanMinimal)?;
.map_err(|_| Error::<T>::AmountOneLessThanMinimal)?;
Self::validate_minimal_amount(amount2.saturating_add(reserve2), &asset2)
.map_err(|_| Error::<T>::AmountLessThanMinimal)?;
.map_err(|_| Error::<T>::AmountTwoLessThanMinimal)?;
Self::transfer(&asset1, &sender, &pool_account, amount1, true)?;
Self::transfer(&asset2, &sender, &pool_account, amount2, true)?;
@@ -635,16 +652,7 @@ pub mod pallet {
let amount_out = *amounts.last().expect("Has always more than 1 element");
ensure!(amount_out >= amount_out_min, Error::<T>::ProvidedMinimumNotSufficientForSwap);
Self::do_swap(&sender, &amounts, &path, &send_to, keep_alive)?;
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,
path,
amount_in,
amount_out,
});
Self::do_swap(sender, &amounts, path, send_to, keep_alive)?;
Ok(())
}
@@ -676,21 +684,13 @@ pub mod pallet {
let amount_in = *amounts.first().expect("Always has more than one element");
ensure!(amount_in <= amount_in_max, Error::<T>::ProvidedMaximumNotSufficientForSwap);
Self::do_swap(&sender, &amounts, &path, &send_to, keep_alive)?;
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,
path,
amount_in,
amount_out,
});
Self::do_swap(sender, &amounts, path, send_to, keep_alive)?;
Ok(())
}
}
impl<T: Config> Pallet<T> {
/// Transfer an `amount` of `asset_id`, respecting the `keep_alive` requirements.
fn transfer(
asset_id: &T::MultiAssetId,
from: &T::AccountId,
@@ -709,8 +709,13 @@ pub mod pallet {
true => Preserve,
false => Expendable,
};
let amount = Self::asset_to_native(amount)?;
Ok(Self::native_to_asset(T::Currency::transfer(from, to, amount, preservation)?)?)
let amount = Self::convert_asset_balance_to_native_balance(amount)?;
Ok(Self::convert_native_balance_to_asset_balance(T::Currency::transfer(
from,
to,
amount,
preservation,
)?)?)
} else {
T::Assets::transfer(
T::MultiAssetIdConverter::try_convert(&asset_id)
@@ -723,31 +728,40 @@ pub mod pallet {
}
}
pub(crate) fn native_to_asset(amount: T::Balance) -> Result<T::AssetBalance, Error<T>> {
/// Convert a `Balance` type to an `AssetBalance`.
pub(crate) fn convert_native_balance_to_asset_balance(
amount: T::Balance,
) -> Result<T::AssetBalance, Error<T>> {
T::HigherPrecisionBalance::from(amount)
.try_into()
.map_err(|_| Error::<T>::Overflow)
}
pub(crate) fn asset_to_native(amount: T::AssetBalance) -> Result<T::Balance, Error<T>> {
/// Convert an `AssetBalance` type to a `Balance`.
pub(crate) fn convert_asset_balance_to_native_balance(
amount: T::AssetBalance,
) -> Result<T::Balance, Error<T>> {
T::HigherPrecisionBalance::from(amount)
.try_into()
.map_err(|_| Error::<T>::Overflow)
}
/// Swap assets along a `path`, depositing in `send_to`.
pub(crate) fn do_swap(
sender: &T::AccountId,
sender: T::AccountId,
amounts: &Vec<T::AssetBalance>,
path: &BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
send_to: &T::AccountId,
path: BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
send_to: T::AccountId,
keep_alive: bool,
) -> Result<(), DispatchError> {
if let Some([asset1, asset2]) = path.get(0..2) {
ensure!(amounts.len() > 1, Error::<T>::CorrespondenceError);
if let Some([asset1, asset2]) = &path.get(0..2) {
let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone());
let pool_account = Self::get_pool_account(&pool_id);
let first_amount = amounts.first().expect("Always has more than one element");
// amounts should always contain a corresponding element to path.
let first_amount = amounts.first().ok_or(Error::<T>::CorrespondenceError)?;
Self::transfer(asset1, sender, &pool_account, *first_amount, keep_alive)?;
Self::transfer(asset1, &sender, &pool_account, *first_amount, keep_alive)?;
let mut i = 0;
let path_len = path.len() as u32;
@@ -757,7 +771,7 @@ pub mod pallet {
let pool_account = Self::get_pool_account(&pool_id);
let amount_out =
amounts.get((i + 1) as usize).ok_or(Error::<T>::PathError)?;
amounts.get((i + 1) as usize).ok_or(Error::<T>::CorrespondenceError)?;
let to = if i < path_len - 2 {
let asset3 = path.get((i + 2) as usize).ok_or(Error::<T>::PathError)?;
@@ -778,6 +792,15 @@ pub mod pallet {
}
i.saturating_inc();
}
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,
path,
amount_in: *first_amount,
amount_out: *amounts.last().expect("Always has more than 1 element"),
});
} else {
return Err(Error::<T>::InvalidPath.into())
}
Ok(())
}
@@ -793,14 +816,16 @@ pub mod pallet {
.expect("infinite length input; no invalid inputs for type; qed")
}
/// Get the `owner`'s balance of `asset`, which could be the chain's native asset or another
/// fungible. Returns a value in the form of an `AssetBalance`.
fn get_balance(
owner: &T::AccountId,
asset: &T::MultiAssetId,
) -> Result<T::AssetBalance, Error<T>> {
if T::MultiAssetIdConverter::is_native(asset) {
Self::native_to_asset(<<T as Config>::Currency>::reducible_balance(
owner, Expendable, Polite,
))
Self::convert_native_balance_to_asset_balance(
<<T as Config>::Currency>::reducible_balance(owner, Expendable, Polite),
)
} else {
Ok(<<T as Config>::Assets>::reducible_balance(
T::MultiAssetIdConverter::try_convert(asset)
@@ -841,6 +866,7 @@ pub mod pallet {
Ok((balance1, balance2))
}
/// Leading to an amount at the end of a `path`, get the required amounts in.
pub(crate) fn get_amounts_in(
amount_out: &T::AssetBalance,
path: &BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
@@ -860,6 +886,7 @@ pub mod pallet {
Ok(amounts)
}
/// Following an amount into a `path`, get the corresponding amounts out.
pub(crate) fn get_amounts_out(
amount_in: &T::AssetBalance,
path: &BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
@@ -969,10 +996,10 @@ pub mod pallet {
result.try_into().map_err(|_| Error::<T>::Overflow)
}
/// Calculates amount out
/// Calculates amount out.
///
/// Given an input amount of an asset and pair reserves, returns the maximum output amount
/// of the other asset
/// of the other asset.
pub fn get_amount_out(
amount_in: &T::AssetBalance,
reserve_in: &T::AssetBalance,
@@ -1004,10 +1031,10 @@ pub mod pallet {
result.try_into().map_err(|_| Error::<T>::Overflow)
}
/// Calculates amount in
/// Calculates amount in.
///
/// Given an output amount of an asset and pair reserves, returns a required input amount
/// of the other asset
/// of the other asset.
pub fn get_amount_in(
amount_out: &T::AssetBalance,
reserve_in: &T::AssetBalance,
@@ -1046,6 +1073,7 @@ pub mod pallet {
result.try_into().map_err(|_| Error::<T>::Overflow)
}
/// Ensure that a `value` meets the minimum balance requirements of an `asset` class.
fn validate_minimal_amount(
value: T::AssetBalance,
asset: &T::MultiAssetId,
@@ -1064,6 +1092,7 @@ pub mod pallet {
Ok(())
}
/// Ensure that a path is valid.
fn validate_swap_path(
path: &BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
) -> Result<(), DispatchError> {
@@ -1092,7 +1121,7 @@ pub mod pallet {
}
impl<T: Config>
frame_support::traits::tokens::fungibles::SwapForNative<
frame_support::traits::tokens::fungibles::SwapNative<
T::RuntimeOrigin,
T::AccountId,
T::Balance,
@@ -1103,7 +1132,11 @@ where
<T as pallet::Config>::Currency:
frame_support::traits::tokens::fungible::Inspect<<T as frame_system::Config>::AccountId>,
{
// If successful returns the amount in.
/// 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(
sender: T::AccountId,
asset_id: T::AssetId,
@@ -1121,26 +1154,59 @@ where
path.push(T::MultiAssetIdConverter::get_native());
let path = path.try_into().unwrap();
let amount_out = Self::native_to_asset(amount_out)?;
// 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().expect("Always has more than one element");
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::<T>::ProvidedMaximumNotSufficientForSwap);
}
Self::do_swap(&sender, &amounts, &path, &send_to, keep_alive)?;
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,
path,
amount_in,
amount_out,
});
Self::do_swap(sender, &amounts, path, send_to, keep_alive)?;
Ok(amount_in)
}
/// 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(
sender: T::AccountId,
asset_id: T::AssetId,
amount_in: T::Balance,
amount_out_min: Option<T::AssetBalance>,
send_to: T::AccountId,
keep_alive: bool,
) -> Result<T::AssetBalance, DispatchError> {
ensure!(amount_in > Zero::zero(), Error::<T>::ZeroAmount);
if let Some(amount_out_min) = amount_out_min {
ensure!(amount_out_min > Zero::zero(), Error::<T>::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::<T>::ProvidedMaximumNotSufficientForSwap);
}
Self::do_swap(sender, &amounts, path, send_to, keep_alive)?;
Ok(amount_out)
}
}
sp_api::decl_runtime_apis! {
@@ -1167,3 +1233,5 @@ sp_api::decl_runtime_apis! {
fn get_reserves(asset1: AssetId, asset2: AssetId) -> Option<(Balance, Balance)>;
}
}
sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $);
@@ -356,7 +356,7 @@ fn add_tiny_liquidity_leads_to_insufficient_liquidity_minted_error() {
1,
user
),
Error::<Test>::AmountLessThanMinimal
Error::<Test>::AmountOneLessThanMinimal
);
assert_noop!(