diff --git a/frame/asset-conversion/src/lib.rs b/frame/asset-conversion/src/lib.rs index f9aeeace11fe7..2d593b3312092 100644 --- a/frame/asset-conversion/src/lib.rs +++ b/frame/asset-conversion/src/lib.rs @@ -51,7 +51,7 @@ //! http://localhost:9933/ //! ``` //! (This can be run against the kitchen sync node in the `node` folder of this repo.) -#![deny(missing_docs)] +// #![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] use frame_support::traits::{DefensiveOption, Incrementable}; @@ -70,7 +70,14 @@ mod mock; use codec::Codec; use frame_support::{ ensure, - traits::tokens::{AssetId, Balance}, + traits::{ + fungible::{ + Balanced as BalancedFungible, Credit as CreditFungible, Inspect as InspectFungible, + Mutate as MutateFungible, + }, + fungibles::{Balanced, Create, Credit, Debt, Inspect, Mutate}, + tokens::{AssetId, Balance}, + }, }; use frame_system::{ ensure_signed, @@ -94,14 +101,12 @@ pub mod pallet { use frame_support::{ pallet_prelude::*, traits::{ - fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, - fungibles::{Create, Inspect, Mutate}, tokens::{ Fortitude::Polite, Precision::Exact, Preservation::{Expendable, Preserve}, }, - AccountTouch, ContainsPair, + AccountTouch, ContainsPair, SameOrOther, }, BoundedBTreeSet, PalletId, }; @@ -121,7 +126,8 @@ pub mod pallet { /// Currency type that this works on. type Currency: InspectFungible - + MutateFungible; + + MutateFungible + + BalancedFungible; /// The `Currency::Balance` type of the native currency. type Balance: Balance; @@ -159,7 +165,8 @@ pub mod pallet { type Assets: Inspect + Mutate + AccountTouch - + ContainsPair; + + ContainsPair + + Balanced; /// Registry for the lp tokens. Ideally only this pallet should have create permissions on /// the assets. @@ -288,6 +295,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, + /// 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. @@ -334,6 +353,8 @@ pub mod pallet { AssetOneWithdrawalDidNotMeetMinimum, /// The minimal amount requirement for the second token in the pair wasn't met. AssetTwoWithdrawalDidNotMeetMinimum, + /// The asset for swap and the originating credit asset id do not match. + AssetCreditMismatch, /// Optimal calculated amount is less than desired. OptimalAmountLessThanDesired, /// Insufficient liquidity minted. @@ -741,6 +762,35 @@ pub mod pallet { Ok(amount_out) } + pub fn do_swap_tokens_for_exact_native_tokens_credit( + path: BoundedVec, + amount_out: T::Balance, + credit_in: Credit, + ) -> Result< + (Credit, CreditFungible), + DispatchError, + > { + ensure!(amount_out > Zero::zero(), Error::::ZeroAmount); + ensure!(credit_in.peek() > Zero::zero(), Error::::ZeroAmount); + + Self::validate_swap_path(&path)?; + + let amounts = Self::get_amounts_in( + &Self::convert_native_balance_to_asset_balance(amount_out)?, + &path, + )?; + let amount_in = + *amounts.first().defensive_ok_or("get_amounts_in() returned an empty result")?; + + ensure!(credit_in.peek() == amount_in, Error::::ProvidedMaximumNotSufficientForSwap); + + let (credit_in, credit_change) = credit_in.split(amount_in); + + let out_credit = Self::do_swap_native_credit(credit_in, &amounts, path)?; + + Ok((credit_change, out_credit)) + } + /// 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. @@ -818,6 +868,81 @@ pub mod pallet { result } + // /// Credit an `amount` of `asset_id` in `to` account. + // fn credit_resolve( + // asset_id: T::MultiAssetId, + // to: &T::AccountId, + // amount: T::AssetBalance, + // ) -> Result<(), DispatchError> + // where + // T::AssetBalance: Balanced, + // (T::MultiAssetId, T::AssetBalance): Into>, + // { + // let credit: Credit = (asset_id, amount).into(); + // + // // Resolve is swallowing the inner Self::deposit DispatchError.. + // T::AssetBalance::resolve(&to, credit).map_err(|_| Error::::Overflow)?; + // + // Ok(()) + // } + + // /// Remove an `amount` of `asset_id` `from` account and return it as a credit imbalance. + // fn credit_settle( + // asset_id: T::MultiAssetId, + // from: &T::AccountId, + // amount: T::AssetBalance, + // ) -> Result, DispatchError> + // where + // T::AssetBalance: Balanced, + // (T::MultiAssetId, T::AssetBalance): Into>, + // { + // let debt: Debt = (asset_id, amount).into(); + // + // // Settle is swallowing the inner Self::deposit DispatchError.. + // Ok(T::AssetBalance::settle(&from, debt, Preserve).map_err(|_| Error::::Overflow)?) + // } + + // /// TODO + // fn credit_resolve( + // asset_id: T::MultiAssetId, + // to: &T::AccountId, + // credit: Credit, + // ) -> Result<(), DispatchError> + // where + // T::AssetBalance: Balanced, + // { + // // TODO similar to Self::transfer, use MultiAssetIdConverter to determine whether to use + // // T::Assets or T:Currency + + // let result = match T::MultiAssetIdConverter::try_convert(&asset_id) { + // MultiAssetIdConversionResult::Converted(asset_id) => + // T::AssetBalance::resolve(&to, credit).map_err(|_| Error::::Overflow)?, + // MultiAssetIdConversionResult::Native => { + // T::Currency::mint_into( + // to, + // Self::convert_asset_balance_to_native_balance(credit.peek())?, + // )?; + // Ok(()) + // }, + // MultiAssetIdConversionResult::Unsupported(_) => + // Err(Error::::UnsupportedAsset.into()), + // }; + + // Ok(result) + // } + + // fn withdraw( + // asset_id: T::MultiAssetId, + // from: &T::AccountId, + // amount: T::AssetBalance, + // ) -> Result, DispatchError> + // where + // T::AssetBalance: Balanced, + // { + // // TODO similar to Self::transfer, use MultiAssetIdConverter to determine whether to use + // // T::Assets or T:Currency + // } + /// Convert a `Balance` type to an `AssetBalance`. pub(crate) fn convert_native_balance_to_asset_balance( amount: T::Balance, @@ -902,6 +1027,138 @@ pub mod pallet { Ok(()) } + // pub(crate) fn do_swap_credit( + // amounts: &Vec, + // path: BoundedVec, + // ) -> Result, DispatchError> + // where + // T::AssetBalance: Balanced, + // (T::MultiAssetId, T::AssetBalance): Into> + // + Into>, + // { + // ensure!(amounts.len() > 1, Error::::CorrespondenceError); + // + // return 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::::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 resolve the credit into the pool_account. + // Self::credit_resolve(asset1.clone(), &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::::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::::ReserveLeftLessThanMinimal)?; + // + // // Same as credit but use settle instead of resolve, and get a Credit<> out + // // TODO: This will only work for path vectors of size 2 (no hops). + // // TODO: Emit a newly created event called SwapCreditExecuted + // return Ok(Self::credit_settle(asset2.clone(), &pool_account, *amount_out)?) + // } + // i.saturating_inc(); + // } + // + // Err(Error::::InvalidPath.into()) + // } else { + // Err(Error::::InvalidPath.into()) + // } + // } + + pub(crate) fn do_swap_native_credit( + credit_in: Credit, + amounts: &Vec, + path: BoundedVec, + ) -> Result, DispatchError> { + ensure!(amounts.len() > 1, Error::::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::::CorrespondenceError)?; + + ensure!( + credit_in.peek() == *first_amount, + Error::::ProvidedMinimumNotSufficientForSwap + ); + // TODO MultiAssetIdConverter::try_convert(asset1) and assert + // ensure!( + // credit_in.asset() == asset1.clone().into(), + // Error::::AssetCreditMismatch + // ); + + // TODO fails if returns back credit + T::Assets::resolve(&pool_account, credit_in); + + 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::::CorrespondenceError)?; + + let to = if i < path_len - 2 { + let asset3 = path.get((i + 2) as usize).ok_or(Error::::PathError)?; + Some(Self::get_pool_account(&Self::get_pool_id( + asset2.clone(), + asset3.clone(), + ))) + } else { + None + }; + + 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::::ReserveLeftLessThanMinimal)?; + + if to.is_some() { + // TODO transfer as in original swap + // Wouldn't this transfer between pool accounts? Shouldn't we do it + // with credits instead? Also, this implies more than one asset hop. + Self::transfer(asset2, &pool_account, &to.unwrap(), *amount_out, true)?; + } else { + return match T::MultiAssetIdConverter::try_convert(asset2) { + MultiAssetIdConversionResult::Native => + // TODO review arguments (Exact, Expendable, Polite). + T::Currency::withdraw( + &pool_account, + Self::convert_asset_balance_to_native_balance(*amount_out)?, + Exact, + Expendable, + Polite, + ), + _ => Err(Error::::InvalidPath.into()), + } + } + } + i.saturating_inc(); + } + + Err(Error::::InvalidPath.into()) + // TODO deposit event + } else { + Err(Error::::InvalidPath.into()) + } + } + /// The account ID of the pool. /// /// This actually does computation. If you need to keep using it, then make sure you cache @@ -1194,7 +1451,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); @@ -1256,6 +1515,29 @@ impl Swap f Ok(amount_out.into()) } + // fn swap_tokens_for_exact_tokens( + // path: Vec, + // amount_out: T::HigherPrecisionBalance, + // credit_in: Credit, + // ) -> Result, DispatchError> + // where + // T::HigherPrecisionBalance: Balanced, + // T::AssetBalance: Balanced, + // (T::MultiAssetId, T::AssetBalance): + // Into> + Into>, + // { + // let path = path.try_into().map_err(|_| Error::::PathError)?; + // let amount_in_ab = Self::convert_hpb_to_asset_balance(credit_in.peek())?; + // let amount_in: Credit = (path[0], amount_in_ab).into(); + // let out = Self::do_swap_tokens_for_exact_tokens_credit( + // path, + // Self::convert_hpb_to_asset_balance(amount_out)?, + // amount_in, + // )?; + // // TODO: convert out credits to HPB + // Ok(out.into()) + // } + fn swap_tokens_for_exact_tokens( sender: T::AccountId, path: Vec, @@ -1278,6 +1560,37 @@ impl Swap f } } +impl SwapCredit + for Pallet +{ + // fn swap_exact_tokens_for_tokens( + // path: Vec, + // credit_in: Credit, + // amount_out_min: Option, + // ) -> Result, DispatchError> + // where + // T::AssetBalance: Balanced, + // { + // let path = path.try_into().map_err(|_| Error::::PathError)?; + // // TODO + // let out = Self::do_swap_tokens_for_exact_tokens_credit(path, amount_out_min, credit_in)?; + // Ok(out.0) + // } + + fn swap_tokens_for_exact_native_tokens( + path: Vec, + amount_out: T::Balance, + credit_in: Credit, + ) -> Result< + (Credit, CreditFungible), + DispatchError, + > { + let path = path.try_into().map_err(|_| Error::::PathError)?; + let out = Self::do_swap_tokens_for_exact_native_tokens_credit(path, amount_out, credit_in)?; + Ok(out) + } +} + sp_api::decl_runtime_apis! { /// This runtime api allows people to query the size of the liquidity pools /// and quote prices for swaps. diff --git a/frame/asset-conversion/src/tests.rs b/frame/asset-conversion/src/tests.rs index 80faf5363b011..450a074ec3675 100644 --- a/frame/asset-conversion/src/tests.rs +++ b/frame/asset-conversion/src/tests.rs @@ -66,7 +66,11 @@ fn pool_assets() -> Vec { fn create_tokens(owner: u128, tokens: Vec>) { for token_id in tokens { - let MultiAssetIdConversionResult::Converted(asset_id) = NativeOrAssetIdConverter::try_convert(&token_id) else { unreachable!("invalid token") }; + let MultiAssetIdConversionResult::Converted(asset_id) = + NativeOrAssetIdConverter::try_convert(&token_id) + else { + unreachable!("invalid token") + }; assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, false, 1)); } } diff --git a/frame/asset-conversion/src/types.rs b/frame/asset-conversion/src/types.rs index 7cd9743ff04b8..f0955dfd9ee2d 100644 --- a/frame/asset-conversion/src/types.rs +++ b/frame/asset-conversion/src/types.rs @@ -18,6 +18,8 @@ use super::*; use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::fungibles::Credit; +use frame_system::Account; use scale_info::TypeInfo; use sp_std::{cmp::Ordering, marker::PhantomData}; @@ -79,6 +81,12 @@ where } } +/// Representation of the final credit imbalance after a swap for exact. +pub struct CreditPair> { + pub in_leftover: Credit, + pub out_credit: Credit, +} + /// 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]`. @@ -98,6 +106,33 @@ pub trait Swap { keep_alive: bool, ) -> Result; + // /// 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` is enough to acquire `amount_out`. + // /// + // /// Uses the `amount_in` imbalance to offset into the pool account. + // /// + // /// If successful, returns the amount of `path[1]` acquired for the `amount_in` + // /// along with the leftovers from `amount_in` as an imbalance. + // /// They could be credited somewhere as the type implies, but can also be dropped. + // /// + // /// Note: This method effectively prevents overswapping, so that the + // /// returned `CreditPair::in_leftover` can then be directly refunded in the initial asset + // /// without swapping back from the `path[1]` asset. + // /// + // /// `amount_in` 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. + // /// + // /// If this function returns an error, no credit will be taken from amount_in, like a no-op. + // fn swap_tokens_for_exact_tokens_credit( + // path: Vec, + // amount_out: Balance, + // credit_in: Credit, + // ) -> Result, DispatchError> + // where + // Balance: Balanced; + /// 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. @@ -116,6 +151,37 @@ pub trait Swap { ) -> Result; } +pub trait SwapCredit +where + Fungible: BalancedFungible, + Fungibles: Balanced, +{ + // fn swap_exact_native_tokens_for_tokens( + // path: Vec, + // credit_in: CreditFungible, + // amount_out_min: Option, + // ) -> Result, DispatchError>; + + // fn swap_native_tokens_for_exact_tokens( + // path: Vec, + // amount_out: AssetBalance, + // credit_in: CreditFungible, + // ) -> Result<(CreditFungible, Credit), + // DispatchError>; + + // fn swap_exact_tokens_for_native_tokens( + // path: Vec, + // credit_in: Credit, + // amount_out_min: Option, + // ) -> Result, DispatchError>; + + fn swap_tokens_for_exact_native_tokens( + path: Vec, + amount_out: Balance, + credit_in: Credit, + ) -> Result<(Credit, CreditFungible), DispatchError>; +} + /// An implementation of MultiAssetId that can be either Native or an asset. #[derive(Decode, Encode, Default, MaxEncodedLen, TypeInfo, Clone, Copy, Debug)] pub enum NativeOrAssetId