Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Added Asset Conversion swap*_credit functions #14716

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 127 additions & 2 deletions frame/asset-conversion/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ mod mock;
use codec::Codec;
use frame_support::{
ensure,
traits::tokens::{AssetId, Balance},
traits::{
fungibles::{Balanced, Credit},
tokens::{AssetId, Balance},
},
};
use frame_system::{
ensure_signed,
Expand Down Expand Up @@ -288,6 +291,18 @@ pub mod pallet {
/// The amount of the second asset that was received.
amount_out: T::AssetBalance,
},
/// Assets have been converted from one to another using credits.
/// Both `SwapExactTokenForTokenCredit and `SwapTokenForExactTokenCredit`
/// will generate this event.
SwapCreditExecuted {
/// The route of asset ids that the swap went through.
/// E.g. A -> Dot -> B
path: BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
/// The amount of the first asset that was swapped.
amount_in: T::AssetBalance,
/// The amount of the second asset that was received.
amount_out: T::AssetBalance,
},
/// An amount has been transferred from one account to another.
Transfer {
/// The account that the assets were transferred from.
Expand Down Expand Up @@ -741,6 +756,27 @@ pub mod pallet {
Ok(amount_out)
}

pub fn do_swap_tokens_for_exact_tokens_credit(
path: BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
amount_in_max: T::AssetBalance,
amount_out: T::AssetBalance,
) -> Result<SwapCredits<T::AccountId, T::AssetBalance>, DispatchError> {
ensure!(amount_out > Zero::zero(), Error::<T>::ZeroAmount);
ensure!(amount_in_max > Zero::zero(), Error::<T>::ZeroAmount);

Self::validate_swap_path(&path)?;

let amounts = Self::get_amounts_in(&amount_out, &path)?;
let amount_in =
*amounts.first().defensive_ok_or("get_amounts_in() returned an empty result")?;

ensure!(amount_in <= amount_in_max, Error::<T>::ProvidedMaximumNotSufficientForSwap);

let out_credit = Self::do_swap_credit(&amounts, path)?;
let in_credit = (path[0].clone(), amount_in_max.saturating_sub(amount_in)?).into();
Ok((in_credit, out_credit).into())
}

/// 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.
Expand Down Expand Up @@ -818,6 +854,33 @@ pub mod pallet {
result
}

/// Credit an `amount` of `asset_id` into `to` account.
fn credit(
asset_id: &T::MultiAssetId,
to: &T::AccountId,
amount: T::AssetBalance,
) -> Result<(), DispatchError> {
let result = match T::MultiAssetIdConverter::try_convert(asset_id) {
MultiAssetIdConversionResult::Converted(asset_id) => {
let credit: Credit<T::AccountId, T::AssetBalance> = (asset_id, amount).into();
<T::Balances as Balanced<_>>::resolve(&to, credit)?;

Ok(())
},
MultiAssetIdConversionResult::Native => {
let amount = Self::convert_asset_balance_to_native_balance(amount)?;
let credit: Credit<T::AccountId, T::Balance> = (asset_id, amount).into();
<T::Balances as Balanced<_>>::resolve(&to, credit)?;

Ok(())
},
MultiAssetIdConversionResult::Unsupported(_) =>
Err(Error::<T>::UnsupportedAsset.into()),
};

result
}

/// Convert a `Balance` type to an `AssetBalance`.
pub(crate) fn convert_native_balance_to_asset_balance(
amount: T::Balance,
Expand Down Expand Up @@ -902,6 +965,51 @@ pub mod pallet {
Ok(())
}

pub(crate) fn do_swap_credit(
amounts: &Vec<T::AssetBalance>,
path: BoundedVec<T::MultiAssetId, T::MaxSwapPathLength>,
) -> Result<Credit<T::AccountId, T::AssetBalance>, DispatchError> {
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);
// amounts should always contain a corresponding element to path.
let first_amount = amounts.first().ok_or(Error::<T>::CorrespondenceError)?;

// Pool account needs enough balance in asset 1 for the swap, but we don't want to
// actually transfer the funds, since that would require a sender account, so we
// instead imply an imbalance in the total issuance, which will be settled later.
Self::credit(asset1, &pool_account, *first_amount)?;

let mut i = 0;
let path_len = path.len() as u32;
for assets_pair in path.windows(2) {
if let [asset1, asset2] = assets_pair {
let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone());
let pool_account = Self::get_pool_account(&pool_id);

let amount_out =
amounts.get((i + 1) as usize).ok_or(Error::<T>::CorrespondenceError)?;

let reserve = Self::get_balance(&pool_account, asset2)?;
let reserve_left = reserve.saturating_sub(*amount_out);
Self::validate_minimal_amount(reserve_left, asset2)
.map_err(|_| Error::<T>::ReserveLeftLessThanMinimal)?;

// TODO:
// Same as credit but use settle instead of resolve, and get a Credit<> out
Self::credit(asset2, &pool_account, *amount_out)?;
}
i.saturating_inc();
}
// TODO: Emit a newly created event called SwapCreditExecuted
} else {
return Err(Error::<T>::InvalidPath.into())
}
// TODO: Return the Credit outstanding from the Settle
Ok((1, 2).into())
}

/// The account ID of the pool.
///
/// This actually does computation. If you need to keep using it, then make sure you cache
Expand Down Expand Up @@ -1194,7 +1302,9 @@ pub mod pallet {
()
);
} else {
let MultiAssetIdConversionResult::Converted(asset_id) = T::MultiAssetIdConverter::try_convert(asset) else {
let MultiAssetIdConversionResult::Converted(asset_id) =
T::MultiAssetIdConverter::try_convert(asset)
else {
return Err(())
};
let minimal = T::Assets::minimum_balance(asset_id);
Expand Down Expand Up @@ -1256,6 +1366,21 @@ impl<T: Config> Swap<T::AccountId, T::HigherPrecisionBalance, T::MultiAssetId> f
Ok(amount_out.into())
}

fn swap_tokens_for_exact_tokens_credit(
path: Vec<T::MultiAssetId>,
amount_in_max: T::HigherPrecisionBalance,
amount_out: T::HigherPrecisionBalance,
) -> Result<SwapCredits<T::AccountId, T::HigherPrecisionBalance>, DispatchError> {
let path = path.try_into().map_err(|_| Error::<T>::PathError)?;
let amount_in_max = Self::convert_hpb_to_asset_balance(amount_in_max)?;
let amount_in = Self::do_swap_tokens_for_exact_tokens_credit(
path,
amount_in_max,
Self::convert_hpb_to_asset_balance(amount_out)?,
)?;
Ok(amount_in.into())
}

fn swap_tokens_for_exact_tokens(
sender: T::AccountId,
path: Vec<T::MultiAssetId>,
Expand Down
32 changes: 32 additions & 0 deletions frame/asset-conversion/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use super::*;

use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::traits::fungibles::Credit;
use scale_info::TypeInfo;
use sp_std::{cmp::Ordering, marker::PhantomData};

Expand Down Expand Up @@ -79,6 +80,12 @@ where
}
}

/// Representation of the final credit imbalance after a swap.
pub struct SwapCredits<AccountId, Balance>(
PatricioNapoli marked this conversation as resolved.
Show resolved Hide resolved
Credit<AccountId, Balance>, // In asset
Credit<AccountId, Balance>, // Out asset
);

/// Trait for providing methods to swap between the various asset classes.
pub trait Swap<AccountId, Balance, MultiAssetId> {
/// Swap exactly `amount_in` of asset `path[0]` for asset `path[1]`.
Expand All @@ -98,6 +105,31 @@ pub trait Swap<AccountId, Balance, MultiAssetId> {
keep_alive: bool,
) -> Result<Balance, DispatchError>;

/// Swap `amount_in_max` of asset `path[0]` for asset `path[1]` declared in `amount_out`.
/// It will return an error if acquiring `amount_out` would be too costly.
///
/// Thus it is on the RPC side to ensure that `amount_in_max` is enough to acquire `amount_out`.
///
/// This method implies that the amount_in is an imbalance of the `path[0]` asset.
PatricioNapoli marked this conversation as resolved.
Show resolved Hide resolved
///
/// Uses the `amount_in_max` imbalance to offset into the pool account.
///
/// If successful, returns the amount of `path[1]` acquired for the `amount_in_max`
/// along with the `amount_in_max` as an imbalance.
PatricioNapoli marked this conversation as resolved.
Show resolved Hide resolved
/// They could be credited somewhere as the type implies, but can also be dropped.
///
/// Note: This method effectively prevents overswapping, so that the
/// returned Credit.0 can then be directly refunded in the initial asset
/// without swapping back from the `path[1]` asset, saving us a bit of gas.
///
/// `amount_in_max` is not optional due to the fact that it is a balance to be offset
/// (credited to the pool), and not an amount to be acquired from a sender.
fn swap_tokens_for_exact_tokens_credit(
path: Vec<MultiAssetId>,
amount_in_max: Balance, // Is there a benefit of changing this to Credit?
PatricioNapoli marked this conversation as resolved.
Show resolved Hide resolved
amount_out: Balance,
PatricioNapoli marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<SwapCredits<AccountId, Balance>, DispatchError>;

/// 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.
Expand Down