diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 943332087627..c47f53d98e3b 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -302,6 +302,8 @@ impl pallet_assets::Config for Runtime { type BenchmarkHelper = (); } +type MaxSwapLength = ConstU32<4>; + impl pallet_asset_conversion::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; @@ -322,7 +324,7 @@ impl pallet_asset_conversion::Config for Runtime { type LPFee = ConstU32<3>; type PalletId = AssetConversionPalletId; type AllowMultiAssetPools = AllowMultiAssetPools; - type MaxSwapPathLength = ConstU32<4>; + type MaxSwapPathLength = MaxSwapLength; type MultiAssetId = Box; type MultiAssetIdConverter = MultiLocationConverter; @@ -1081,14 +1083,15 @@ impl_runtime_apis! { Balance, u128, Box, + MaxSwapLength > for Runtime { - fn quote_price_exact_tokens_for_tokens(asset1: Box, asset2: Box, amount: u128, include_fee: bool) -> Option { - AssetConversion::quote_price_exact_tokens_for_tokens(asset1, asset2, amount, include_fee) + fn quote_price_exact_tokens_for_tokens(path: BoundedVec, MaxSwapLength>, amount: u128, include_fee: bool) -> Option { + AssetConversion::quote_price_exact_tokens_for_tokens(path, amount, include_fee) } - fn quote_price_tokens_for_exact_tokens(asset1: Box, asset2: Box, amount: u128, include_fee: bool) -> Option { - AssetConversion::quote_price_tokens_for_exact_tokens(asset1, asset2, amount, include_fee) + fn quote_price_tokens_for_exact_tokens(path: BoundedVec, MaxSwapLength>, amount: u128, include_fee: bool) -> Option { + AssetConversion::quote_price_tokens_for_exact_tokens(path, amount, include_fee) } fn get_reserves(asset1: Box, asset2: Box) -> Option<(Balance, Balance)> { diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 2070e3f12d04..6f5832471e15 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -1639,6 +1639,8 @@ parameter_types! { pub const LiquidityWithdrawalFee: Permill = Permill::from_percent(0); // should be non-zero if AllowMultiAssetPools is true, otherwise can be zero. } +type MaxSwapLength = ConstU32<4>; + impl pallet_asset_conversion::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -1657,7 +1659,7 @@ impl pallet_asset_conversion::Config for Runtime { type LiquidityWithdrawalFee = LiquidityWithdrawalFee; type WeightInfo = pallet_asset_conversion::weights::SubstrateWeight; type AllowMultiAssetPools = AllowMultiAssetPools; - type MaxSwapPathLength = ConstU32<4>; + type MaxSwapPathLength = MaxSwapLength; type MintMinLiquidity = MintMinLiquidity; type MultiAssetIdConverter = NativeOrAssetIdConverter; #[cfg(feature = "runtime-benchmarks")] @@ -2561,15 +2563,16 @@ impl_runtime_apis! { Block, Balance, u128, - NativeOrAssetId + NativeOrAssetId, + MaxSwapLength > for Runtime { - fn quote_price_exact_tokens_for_tokens(asset1: NativeOrAssetId, asset2: NativeOrAssetId, amount: u128, include_fee: bool) -> Option { - AssetConversion::quote_price_exact_tokens_for_tokens(asset1, asset2, amount, include_fee) + fn quote_price_exact_tokens_for_tokens(path: BoundedVec, MaxSwapLength>, amount: u128, include_fee: bool) -> Option { + AssetConversion::quote_price_exact_tokens_for_tokens(path, amount, include_fee) } - fn quote_price_tokens_for_exact_tokens(asset1: NativeOrAssetId, asset2: NativeOrAssetId, amount: u128, include_fee: bool) -> Option { - AssetConversion::quote_price_tokens_for_exact_tokens(asset1, asset2, amount, include_fee) + fn quote_price_tokens_for_exact_tokens(path: BoundedVec, MaxSwapLength>, amount: u128, include_fee: bool) -> Option { + AssetConversion::quote_price_tokens_for_exact_tokens(path, amount, include_fee) } fn get_reserves(asset1: NativeOrAssetId, asset2: NativeOrAssetId) -> Option<(Balance, Balance)> { diff --git a/substrate/frame/asset-conversion/src/lib.rs b/substrate/frame/asset-conversion/src/lib.rs index 8d811473e861..f94a61b70783 100644 --- a/substrate/frame/asset-conversion/src/lib.rs +++ b/substrate/frame/asset-conversion/src/lib.rs @@ -82,7 +82,7 @@ use sp_runtime::{ traits::{ CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Ensure, MaybeDisplay, TrailingZeroInput, }, - DispatchError, + BoundedVec, DispatchError, }; use sp_std::prelude::*; pub use types::*; @@ -1018,48 +1018,44 @@ pub mod pallet { /// Used by the RPC service to provide current prices. pub fn quote_price_exact_tokens_for_tokens( - asset1: T::MultiAssetId, - asset2: T::MultiAssetId, - amount: T::AssetBalance, + path: BoundedVec, + amount_in: T::AssetBalance, include_fee: bool, ) -> Option { - let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); - let pool_account = Self::get_pool_account(&pool_id); - - let balance1 = Self::get_balance(&pool_account, &asset1).ok()?; - let balance2 = Self::get_balance(&pool_account, &asset2).ok()?; - if !balance1.is_zero() { - if include_fee { - Self::get_amount_out(&amount, &balance1, &balance2).ok() - } else { - Self::quote(&amount, &balance1, &balance2).ok() - } + Self::validate_swap_path(&path).ok()?; + Some(if include_fee { + *Self::get_amounts_out(&amount_in, &path).ok()?.last()? } else { - None - } + let mut result = amount_in; + for assets_pair in path.windows(2).rev() { + if let [asset1, asset2] = assets_pair { + let (reserve_in, reserve_out) = Self::get_reserves(asset1, asset2).ok()?; + result = Self::quote(&reserve_out, &reserve_in, &result).ok()?; + } + } + result + }) } /// Used by the RPC service to provide current prices. pub fn quote_price_tokens_for_exact_tokens( - asset1: T::MultiAssetId, - asset2: T::MultiAssetId, - amount: T::AssetBalance, + path: BoundedVec, + amount_out: T::AssetBalance, include_fee: bool, ) -> Option { - let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); - let pool_account = Self::get_pool_account(&pool_id); - - let balance1 = Self::get_balance(&pool_account, &asset1).ok()?; - let balance2 = Self::get_balance(&pool_account, &asset2).ok()?; - if !balance1.is_zero() { - if include_fee { - Self::get_amount_in(&amount, &balance1, &balance2).ok() - } else { - Self::quote(&amount, &balance2, &balance1).ok() - } + Self::validate_swap_path(&path).ok()?; + Some(if include_fee { + *Self::get_amounts_in(&amount_out, &path).ok()?.first()? } else { - None - } + let mut result = amount_out; + for assets_pair in path.windows(2).rev() { + if let [asset1, asset2] = assets_pair { + let (reserve_in, reserve_out) = Self::get_reserves(asset1, asset2).ok()?; + result = Self::quote(&reserve_in, &reserve_out, &result).ok()?; + } + } + result + }) } /// Calculates the optimal amount from the reserves. @@ -1285,7 +1281,7 @@ impl Swap f sp_api::decl_runtime_apis! { /// This runtime api allows people to query the size of the liquidity pools /// and quote prices for swaps. - pub trait AssetConversionApi where + pub trait AssetConversionApi where Balance: Codec + MaybeDisplay, AssetBalance: frame_support::traits::tokens::Balance, AssetId: Codec @@ -1294,13 +1290,13 @@ sp_api::decl_runtime_apis! { /// /// Note that the price may have changed by the time the transaction is executed. /// (Use `amount_in_max` to control slippage.) - fn quote_price_tokens_for_exact_tokens(asset1: AssetId, asset2: AssetId, amount: AssetBalance, include_fee: bool) -> Option; + fn quote_price_tokens_for_exact_tokens(path: BoundedVec, amount: AssetBalance, include_fee: bool) -> Option; /// Provides a quote for [`Pallet::swap_exact_tokens_for_tokens`]. /// /// Note that the price may have changed by the time the transaction is executed. /// (Use `amount_out_min` to control slippage.) - fn quote_price_exact_tokens_for_tokens(asset1: AssetId, asset2: AssetId, amount: AssetBalance, include_fee: bool) -> Option; + fn quote_price_exact_tokens_for_tokens(path: BoundedVec, amount: AssetBalance, include_fee: bool) -> Option; /// Returns the size of the liquidity pool for the given asset pair. fn get_reserves(asset1: AssetId, asset2: AssetId) -> Option<(Balance, Balance)>; diff --git a/substrate/frame/asset-conversion/src/tests.rs b/substrate/frame/asset-conversion/src/tests.rs index 1c1267ab87b3..073b0a03924a 100644 --- a/substrate/frame/asset-conversion/src/tests.rs +++ b/substrate/frame/asset-conversion/src/tests.rs @@ -22,6 +22,7 @@ use frame_support::{ traits::{fungible::Inspect, fungibles::InspectEnumerable, Get}, }; use sp_arithmetic::Permill; +use sp_core::bounded_vec::BoundedVec; use sp_runtime::{DispatchError, TokenError}; fn events() -> Vec> { @@ -542,6 +543,10 @@ fn can_quote_price() { let user = 1; let token_1 = NativeOrAssetId::Native; let token_2 = NativeOrAssetId::Asset(2); + let asset_to_native: BoundedVec<_, _> = + bvec![NativeOrAssetId::Asset(2), NativeOrAssetId::Native]; + let native_to_asset: BoundedVec<_, _> = + bvec![NativeOrAssetId::Native, NativeOrAssetId::Asset(2)]; create_tokens(user, vec![token_2]); assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); @@ -562,8 +567,7 @@ fn can_quote_price() { assert_eq!( AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), 3000, false, ), @@ -572,8 +576,7 @@ fn can_quote_price() { // including fee so should get less out... assert_eq!( AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), 3000, true, ), @@ -583,8 +586,7 @@ fn can_quote_price() { // (if the above accidentally exchanged then it would not give same quote as before) assert_eq!( AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), 3000, false, ), @@ -593,8 +595,7 @@ fn can_quote_price() { // including fee so should get less out... assert_eq!( AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), 3000, true, ), @@ -604,21 +605,16 @@ fn can_quote_price() { // Check inverse: assert_eq!( AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Asset(2), - NativeOrAssetId::Native, + asset_to_native.clone(), 60, false, ), Some(3000) ); + // including fee so should get less out... assert_eq!( - AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Asset(2), - NativeOrAssetId::Native, - 60, - true, - ), + AssetConversion::quote_price_exact_tokens_for_tokens(asset_to_native.clone(), 60, true), Some(2302) ); @@ -627,8 +623,7 @@ fn can_quote_price() { // assert_eq!( AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), 60, false, ), @@ -636,20 +631,14 @@ fn can_quote_price() { ); // including fee so should need to put more in... assert_eq!( - AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), - 60, - true, - ), + AssetConversion::quote_price_tokens_for_exact_tokens(native_to_asset.clone(), 60, true), Some(4299) ); // Check it still gives same price: // (if the above accidentally exchanged then it would not give same quote as before) assert_eq!( AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), 60, false, ), @@ -657,20 +646,14 @@ fn can_quote_price() { ); // including fee so should need to put more in... assert_eq!( - AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), - 60, - true, - ), + AssetConversion::quote_price_tokens_for_exact_tokens(native_to_asset.clone(), 60, true), Some(4299) ); // Check inverse: assert_eq!( AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(2), - NativeOrAssetId::Native, + asset_to_native.clone(), 3000, false, ), @@ -679,8 +662,7 @@ fn can_quote_price() { // including fee so should need to put more in... assert_eq!( AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(2), - NativeOrAssetId::Native, + asset_to_native.clone(), 3000, true, ), @@ -694,14 +676,12 @@ fn can_quote_price() { assert_eq!( AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Asset(2), - NativeOrAssetId::Native, + asset_to_native.clone(), amount_in, false, ) .and_then(|amount| AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), amount, false, )), @@ -709,14 +689,12 @@ fn can_quote_price() { ); assert_eq!( AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), amount_in, false, ) .and_then(|amount| AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Asset(2), - NativeOrAssetId::Native, + asset_to_native.clone(), amount, false, )), @@ -725,37 +703,87 @@ fn can_quote_price() { assert_eq!( AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(2), - NativeOrAssetId::Native, + asset_to_native.clone(), amount_in, false, ) .and_then(|amount| AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), + native_to_asset.clone(), amount, false, )), Some(amount_in) ); assert_eq!( - AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(2), - amount_in, - false, - ) - .and_then(|amount| AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(2), - NativeOrAssetId::Native, - amount, - false, - )), + AssetConversion::quote_price_tokens_for_exact_tokens(native_to_asset, amount_in, false) + .and_then(|amount| AssetConversion::quote_price_tokens_for_exact_tokens( + asset_to_native, + amount, + false, + )), Some(amount_in) ); }); } +#[test] +fn quote_price_between_two_non_native_currencies() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + let token_3 = NativeOrAssetId::Asset(3); + + create_tokens(user, vec![token_2, token_3]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_3)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 100000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 200, + 1, + 1, + user, + )); + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_3, + 10000, + 400, + 1, + 1, + user, + )); + + let amount = 10; + let quoted_price = 18; + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + bvec![token_2, token_1, token_3], + amount, + true, + ), + Some(quoted_price) + ); + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + bvec![token_2, token_1, token_3], + quoted_price, + true, + ), + Some(amount) + ); + }); +} + #[test] fn quote_price_exact_tokens_for_tokens_matches_execution() { new_test_ext().execute_with(|| { @@ -784,7 +812,11 @@ fn quote_price_exact_tokens_for_tokens_matches_execution() { let amount = 1; let quoted_price = 49; assert_eq!( - AssetConversion::quote_price_exact_tokens_for_tokens(token_2, token_1, amount, true,), + AssetConversion::quote_price_exact_tokens_for_tokens( + bvec![token_2, token_1], + amount, + true, + ), Some(quoted_price) ); @@ -832,7 +864,11 @@ fn quote_price_tokens_for_exact_tokens_matches_execution() { let amount = 49; let quoted_price = 1; assert_eq!( - AssetConversion::quote_price_tokens_for_exact_tokens(token_2, token_1, amount, true,), + AssetConversion::quote_price_tokens_for_exact_tokens( + vec![token_2, token_1].try_into().unwrap(), + amount, + true, + ), Some(quoted_price) ); diff --git a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs index 9e9b74a0ddb2..9268d83a4d68 100644 --- a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs +++ b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/tests.rs @@ -138,6 +138,12 @@ const WEIGHT_5: Weight = Weight::from_parts(5, 0); const WEIGHT_50: Weight = Weight::from_parts(50, 0); const WEIGHT_100: Weight = Weight::from_parts(100, 0); +macro_rules! bvec { + ($( $x:tt )*) => { + vec![$( $x )*].try_into().unwrap() + } +} + #[test] fn transaction_payment_in_native_possible() { let base_weight = 5; @@ -215,8 +221,7 @@ fn transaction_payment_in_asset_possible() { let fee_in_native = base_weight + tx_weight + len as u64; let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(asset_id), - NativeOrAssetId::Native, + bvec![NativeOrAssetId::Asset(asset_id), NativeOrAssetId::Native], fee_in_native, true, ); @@ -324,8 +329,7 @@ fn transaction_payment_without_fee() { let len = 10; let fee_in_native = base_weight + weight + len as u64; let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(asset_id), - NativeOrAssetId::Native, + bvec![NativeOrAssetId::Asset(asset_id), NativeOrAssetId::Native], fee_in_native, true, ); @@ -342,8 +346,7 @@ fn transaction_payment_without_fee() { assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset); let refund = AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(asset_id), + bvec![NativeOrAssetId::Native, NativeOrAssetId::Asset(asset_id)], fee_in_native, true, ) @@ -399,8 +402,7 @@ fn asset_transaction_payment_with_tip_and_refund() { let len = 10; let fee_in_native = base_weight + weight + len as u64 + tip; let input_quote = AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(asset_id), - NativeOrAssetId::Native, + bvec![NativeOrAssetId::Asset(asset_id), NativeOrAssetId::Native], fee_in_native, true, ); @@ -415,8 +417,7 @@ fn asset_transaction_payment_with_tip_and_refund() { let final_weight = 50; let expected_fee = fee_in_native - final_weight - tip; let expected_token_refund = AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(asset_id), + bvec![NativeOrAssetId::Native, NativeOrAssetId::Asset(asset_id)], fee_in_native - expected_fee - tip, true, ) @@ -480,8 +481,7 @@ fn payment_from_account_with_only_assets() { let fee_in_native = base_weight + weight + len as u64; let ed = Balances::minimum_balance(); let fee_in_asset = AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(asset_id), - NativeOrAssetId::Native, + bvec![NativeOrAssetId::Asset(asset_id), NativeOrAssetId::Native], fee_in_native + ed, true, ) @@ -496,8 +496,7 @@ fn payment_from_account_with_only_assets() { assert_eq!(Assets::balance(asset_id, caller), balance - fee_in_asset); let refund = AssetConversion::quote_price_exact_tokens_for_tokens( - NativeOrAssetId::Native, - NativeOrAssetId::Asset(asset_id), + bvec![NativeOrAssetId::Native, NativeOrAssetId::Asset(asset_id)], ed, true, ) @@ -572,8 +571,7 @@ fn converted_fee_is_never_zero_if_input_fee_is_not() { // validate even a small fee gets converted to asset. let fee_in_native = base_weight + weight + len as u64; let fee_in_asset = AssetConversion::quote_price_tokens_for_exact_tokens( - NativeOrAssetId::Asset(asset_id), - NativeOrAssetId::Native, + bvec![NativeOrAssetId::Asset(asset_id), NativeOrAssetId::Native], fee_in_native, true, )