diff --git a/Cargo.lock b/Cargo.lock index 79f2b5e9092c2..0ac95cdb80e38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3525,6 +3525,17 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-num-traits" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951641f13f873bff03d4bf19ae8bec531935ac0ac2cc775f84d7edfdcfed3f17" +dependencies = [ + "integer-sqrt", + "num-traits", + "uint", +] + [[package]] name = "impl-serde" version = "0.4.0" @@ -3893,6 +3904,7 @@ dependencies = [ "log", "node-primitives", "pallet-alliance", + "pallet-asset-conversion", "pallet-asset-rate", "pallet-asset-tx-payment", "pallet-assets", @@ -3962,6 +3974,7 @@ dependencies = [ "pallet-vesting", "pallet-whitelist", "parity-scale-codec", + "primitive-types", "scale-info", "sp-api", "sp-authority-discovery", @@ -5242,6 +5255,7 @@ dependencies = [ "node-inspect", "node-primitives", "node-rpc", + "pallet-asset-conversion", "pallet-asset-tx-payment", "pallet-assets", "pallet-balances", @@ -5524,6 +5538,7 @@ dependencies = [ "log", "node-executor", "node-primitives", + "pallet-asset-conversion", "pallet-asset-tx-payment", "pallet-assets", "pallet-transaction-payment", @@ -5829,6 +5844,26 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-asset-conversion" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-assets", + "pallet-balances", + "parity-scale-codec", + "primitive-types", + "scale-info", + "sp-api", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-asset-rate" version = "4.0.0-dev" @@ -7912,6 +7947,7 @@ checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" dependencies = [ "fixed-hash", "impl-codec", + "impl-num-traits", "impl-serde", "scale-info", "uint", diff --git a/Cargo.toml b/Cargo.toml index 0ac6a9dfb57e4..eabedd13918a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ members = [ "client/transaction-pool/api", "client/utils", "frame/alliance", + "frame/asset-conversion", "frame/assets", "frame/atomic-swap", "frame/aura", diff --git a/bin/node/cli/Cargo.toml b/bin/node/cli/Cargo.toml index ca9b25b6e75f3..3ab9bb33969e5 100644 --- a/bin/node/cli/Cargo.toml +++ b/bin/node/cli/Cargo.toml @@ -88,6 +88,7 @@ sc-storage-monitor = { version = "0.1.0", path = "../../../client/storage-monito frame-system = { version = "4.0.0-dev", path = "../../../frame/system" } frame-system-rpc-runtime-api = { version = "4.0.0-dev", path = "../../../frame/system/rpc/runtime-api" } pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" } +pallet-asset-conversion = { version = "4.0.0-dev", path = "../../../frame/asset-conversion" } pallet-assets = { version = "4.0.0-dev", path = "../../../frame/assets/" } pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment/" } pallet-im-online = { version = "4.0.0-dev", default-features = false, path = "../../../frame/im-online" } diff --git a/bin/node/cli/src/chain_spec.rs b/bin/node/cli/src/chain_spec.rs index 5a91d9c0f5920..08ccd3ee93aa0 100644 --- a/bin/node/cli/src/chain_spec.rs +++ b/bin/node/cli/src/chain_spec.rs @@ -363,6 +363,7 @@ pub fn testnet_genesis( assets: vec![(9, get_account_id_from_seed::("Alice"), true, 1)], ..Default::default() }, + pool_assets: Default::default(), transaction_storage: Default::default(), transaction_payment: Default::default(), alliance: Default::default(), diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 9708e1ed9fcdc..664b26798f2cd 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -24,6 +24,9 @@ scale-info = { version = "2.5.0", default-features = false, features = ["derive" static_assertions = "1.1.0" log = { version = "0.4.17", default-features = false } +# pallet-asset-conversion: turn on "num-traits" feature +primitive-types = { version = "0.12.0", default-features = false, features = ["codec", "scale-info", "num-traits"] } + # primitives sp-authority-discovery = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/authority-discovery" } sp-consensus-babe = { version = "0.10.0-dev", default-features = false, path = "../../../primitives/consensus/babe" } @@ -54,6 +57,7 @@ frame-election-provider-support = { version = "4.0.0-dev", default-features = fa frame-system-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/system/rpc/runtime-api/" } frame-try-runtime = { version = "0.10.0-dev", default-features = false, path = "../../../frame/try-runtime", optional = true } pallet-alliance = { version = "4.0.0-dev", default-features = false, path = "../../../frame/alliance" } +pallet-asset-conversion = { version = "4.0.0-dev", default-features = false, path = "../../../frame/asset-conversion" } pallet-asset-rate = { version = "4.0.0-dev", default-features = false, path = "../../../frame/asset-rate" } pallet-assets = { version = "4.0.0-dev", default-features = false, path = "../../../frame/assets" } pallet-authority-discovery = { version = "4.0.0-dev", default-features = false, path = "../../../frame/authority-discovery" } @@ -137,6 +141,7 @@ std = [ "frame-system-benchmarking?/std", "frame-election-provider-support/std", "sp-authority-discovery/std", + "pallet-asset-conversion/std", "pallet-assets/std", "pallet-authority-discovery/std", "pallet-authorship/std", @@ -236,6 +241,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-alliance/runtime-benchmarks", + "pallet-asset-conversion/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", @@ -297,6 +303,7 @@ try-runtime = [ "frame-system/try-runtime", "frame-support/try-runtime", "pallet-alliance/try-runtime", + "pallet-asset-conversion/try-runtime", "pallet-assets/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 21645c5aa7dcd..6dc9841f6b44f 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -29,6 +29,8 @@ use frame_election_provider_support::{ use frame_support::{ construct_runtime, dispatch::DispatchClass, + instances::{Instance1, Instance2}, + ord_parameter_types, pallet_prelude::Get, parameter_types, traits::{ @@ -48,10 +50,11 @@ use frame_support::{ }; use frame_system::{ limits::{BlockLength, BlockWeights}, - EnsureRoot, EnsureRootWithSuccess, EnsureSigned, EnsureWithSuccess, + EnsureRoot, EnsureRootWithSuccess, EnsureSigned, EnsureSignedBy, EnsureWithSuccess, }; pub use node_primitives::{AccountId, Signature}; use node_primitives::{AccountIndex, Balance, BlockNumber, Hash, Index, Moment}; +use pallet_asset_conversion::{NativeOrAssetId, NativeOrAssetIdConverter}; use pallet_election_provider_multi_phase::SolutionAccuracyOf; use pallet_im_online::sr25519::AuthorityId as ImOnlineId; use pallet_nfts::PalletFeatures; @@ -69,8 +72,8 @@ use sp_runtime::{ curve::PiecewiseLinear, generic, impl_opaque_keys, traits::{ - self, BlakeTwo256, Block as BlockT, Bounded, ConvertInto, NumberFor, OpaqueKeys, - SaturatedConversion, StaticLookup, + self, AccountIdConversion, BlakeTwo256, Block as BlockT, Bounded, ConvertInto, NumberFor, + OpaqueKeys, SaturatedConversion, StaticLookup, }, transaction_validity::{TransactionPriority, TransactionSource, TransactionValidity}, ApplyExtrinsicResult, FixedPointNumber, FixedU128, Perbill, Percent, Permill, Perquintill, @@ -482,7 +485,7 @@ impl pallet_asset_tx_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Fungibles = Assets; type OnChargeAssetTransaction = pallet_asset_tx_payment::FungiblesAdapter< - pallet_assets::BalanceToAssetBalance, + pallet_assets::BalanceToAssetBalance, CreditToBlockAuthor, >; } @@ -1473,7 +1476,7 @@ parameter_types! { pub const MetadataDepositPerByte: Balance = 1 * DOLLARS; } -impl pallet_assets::Config for Runtime { +impl pallet_assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = u128; type AssetId = u32; @@ -1496,6 +1499,66 @@ impl pallet_assets::Config for Runtime { type BenchmarkHelper = (); } +ord_parameter_types! { + pub const AssetConversionOrigin: AccountId = AccountIdConversion::::into_account_truncating(&AssetConversionPalletId::get()); +} + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = u128; + type AssetId = u32; + type AssetIdParameter = codec::Compact; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + type AssetDeposit = AssetDeposit; + type AssetAccountDeposit = ConstU128; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = StringLimit; + type Freezer = (); + type Extra = (); + type WeightInfo = pallet_assets::weights::SubstrateWeight; + type RemoveItemsLimit = ConstU32<1000>; + type CallbackHandle = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +parameter_types! { + pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon"); + pub AllowMultiAssetPools: bool = true; + pub const PoolSetupFee: Balance = 1 * DOLLARS; // should be more or equal to the existential deposit + pub const MintMinLiquidity: Balance = 100; // 100 is good enough when the main currency has 10-12 decimals. + pub const LiquidityWithdrawalFee: Permill = Permill::from_percent(0); // should be non-zero if AllowMultiAssetPools is true, otherwise can be zero. +} + +impl pallet_asset_conversion::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type AssetBalance = ::Balance; + type HigherPrecisionBalance = sp_core::U256; + type Assets = Assets; + type Balance = u128; + type PoolAssets = PoolAssets; + type AssetId = >::AssetId; + type MultiAssetId = NativeOrAssetId; + type PoolAssetId = >::AssetId; + type PalletId = AssetConversionPalletId; + type LPFee = ConstU32<3>; // means 0.3% + type PoolSetupFee = PoolSetupFee; + type PoolSetupFeeReceiver = AssetConversionOrigin; + type LiquidityWithdrawalFee = LiquidityWithdrawalFee; + type WeightInfo = pallet_asset_conversion::weights::SubstrateWeight; + type AllowMultiAssetPools = AllowMultiAssetPools; + type MaxSwapPathLength = ConstU32<4>; + type MintMinLiquidity = MintMinLiquidity; + type MultiAssetIdConverter = NativeOrAssetIdConverter; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + parameter_types! { pub const QueueCount: u32 = 300; pub const MaxQueueLen: u32 = 1000; @@ -1617,7 +1680,7 @@ impl pallet_nft_fractionalization::Config for Runtime { type NftCollectionId = ::CollectionId; type NftId = ::ItemId; type AssetBalance = ::Balance; - type AssetId = ::AssetId; + type AssetId = >::AssetId; type Assets = Assets; type Nfts = Nfts; type PalletId = NftFractionalizationPalletId; @@ -1838,7 +1901,8 @@ construct_runtime!( Multisig: pallet_multisig, Bounties: pallet_bounties, Tips: pallet_tips, - Assets: pallet_assets, + Assets: pallet_assets::, + PoolAssets: pallet_assets::, Mmr: pallet_mmr, Lottery: pallet_lottery, Nis: pallet_nis, @@ -1861,6 +1925,7 @@ construct_runtime!( NominationPools: pallet_nomination_pools, RankedPolls: pallet_referenda::, RankedCollective: pallet_ranked_collective, + AssetConversion: pallet_asset_conversion, FastUnstake: pallet_fast_unstake, MessageQueue: pallet_message_queue, Pov: frame_benchmarking_pallet_pov, @@ -1951,6 +2016,7 @@ mod benches { [pallet_contracts, Contracts] [pallet_core_fellowship, CoreFellowship] [pallet_democracy, Democracy] + [pallet_asset_conversion, AssetConversion] [pallet_election_provider_multi_phase, ElectionProviderMultiPhase] [pallet_election_provider_support_benchmarking, EPSBench::] [pallet_elections_phragmen, Elections] @@ -2288,6 +2354,26 @@ impl_runtime_apis! { } } + impl pallet_asset_conversion::AssetConversionApi< + Block, + Balance, + u128, + NativeOrAssetId + > 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_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 get_reserves(asset1: NativeOrAssetId, asset2: NativeOrAssetId) -> Option<(Balance, Balance)> { + AssetConversion::get_reserves(&asset1, &asset2).ok() + } + } + impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentCallApi for Runtime { diff --git a/bin/node/testing/Cargo.toml b/bin/node/testing/Cargo.toml index a43b2b9ba13e5..6c9f1b161104d 100644 --- a/bin/node/testing/Cargo.toml +++ b/bin/node/testing/Cargo.toml @@ -22,6 +22,7 @@ frame-system = { version = "4.0.0-dev", path = "../../../frame/system" } node-executor = { version = "3.0.0-dev", path = "../executor" } node-primitives = { version = "2.0.0", path = "../primitives" } kitchensink-runtime = { version = "3.0.0-dev", path = "../runtime" } +pallet-asset-conversion = { version = "4.0.0-dev", path = "../../../frame/asset-conversion" } pallet-assets = { version = "4.0.0-dev", path = "../../../frame/assets" } pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment" } pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" } diff --git a/bin/node/testing/src/genesis.rs b/bin/node/testing/src/genesis.rs index f91070017c0f8..51e6a8cfd09c7 100644 --- a/bin/node/testing/src/genesis.rs +++ b/bin/node/testing/src/genesis.rs @@ -89,6 +89,7 @@ pub fn config_endowed(code: Option<&[u8]>, extra_endowed: Vec) -> Gen society: SocietyConfig { members: vec![alice(), bob()], pot: 0, max_members: 999 }, vesting: Default::default(), assets: AssetsConfig { assets: vec![(9, alice(), true, 1)], ..Default::default() }, + pool_assets: Default::default(), transaction_storage: Default::default(), transaction_payment: Default::default(), alliance: Default::default(), diff --git a/frame/asset-conversion/Cargo.toml b/frame/asset-conversion/Cargo.toml new file mode 100644 index 0000000000000..e29a2c5b3b7ce --- /dev/null +++ b/frame/asset-conversion/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "pallet-asset-conversion" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME asset conversion pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } +scale-info = { version = "2.0.0", default-features = false, features = ["derive"] } +sp-api = { version = "4.0.0-dev", default-features = false, path = "../../primitives/api" } +sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-arithmetic = { version = "6.0.0", default-features = false, path = "../../primitives/arithmetic" } + +[dev-dependencies] +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +pallet-assets = { version = "4.0.0-dev", path = "../assets" } +primitive-types = { version = "0.12.0", default-features = false, features = ["codec", "scale-info", "num-traits"] } +sp-std = { version = "5.0.0", path = "../../primitives/std" } +sp-core = { version = "7.0.0", path = "../../primitives/core" } +sp-io = { version = "7.0.0", path = "../../primitives/io" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-std/std", + "sp-runtime/std", + "sp-arithmetic/std" +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/asset-conversion/README.md b/frame/asset-conversion/README.md new file mode 100644 index 0000000000000..e895db5e83adf --- /dev/null +++ b/frame/asset-conversion/README.md @@ -0,0 +1,25 @@ +# asset-conversion + +## A swap pallet + +This pallet allows assets to be converted from one type to another by means of a constant product formula. +The pallet based is based on [Uniswap V2](https://github.com/Uniswap/v2-core) logic. + +### Overview + +This pallet allows you to: + + - create a liquidity pool for 2 assets + - provide the liquidity and receive back an LP token + - exchange the LP token back to assets + - swap 2 assets if there is a pool created + - query for an exchange price via a new runtime call endpoint + - query the size of a liquidity pool. + +Please see the rust module documentation for full details: + +`cargo doc -p pallet-asset-conversion --open` + +### License + +License: Apache-2.0 diff --git a/frame/asset-conversion/src/benchmarking.rs b/frame/asset-conversion/src/benchmarking.rs new file mode 100644 index 0000000000000..08afc1a46992f --- /dev/null +++ b/frame/asset-conversion/src/benchmarking.rs @@ -0,0 +1,287 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Asset Conversion pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_benchmarking::{benchmarks, whitelisted_caller}; +use frame_support::{ + assert_ok, + storage::bounded_vec::BoundedVec, + traits::{ + fungible::{Inspect as InspectFungible, Unbalanced}, + fungibles::{Create, Inspect, Mutate}, + }, +}; +use frame_system::RawOrigin as SystemOrigin; +use sp_runtime::traits::{Bounded, StaticLookup}; +use sp_std::prelude::*; + +use crate::Pallet as AssetConversion; + +const INITIAL_ASSET_BALANCE: u128 = 1_000_000_000_000; +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; +type BalanceOf = + <::Currency as InspectFungible<::AccountId>>::Balance; + +fn get_lp_token_id() -> T::PoolAssetId +where + T::PoolAssetId: Into, +{ + let next_id: u32 = AssetConversion::::get_next_pool_asset_id().into(); + (next_id - 1).into() +} + +fn create_asset(asset: &T::MultiAssetId) -> (T::AccountId, AccountIdLookupOf) +where + T::AssetBalance: From, + T::Currency: Unbalanced, + T::Assets: Create + Mutate, +{ + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + if let Ok(asset_id) = T::MultiAssetIdConverter::try_convert(asset) { + assert_ok!(T::Currency::write_balance(&caller, BalanceOf::::max_value())); + assert_ok!(T::Assets::create(asset_id.clone(), caller.clone(), true, 1.into())); + assert_ok!(T::Assets::mint_into(asset_id, &caller, INITIAL_ASSET_BALANCE.into())); + } + (caller, caller_lookup) +} + +fn create_asset_and_pool( + asset1: &T::MultiAssetId, + asset2: &T::MultiAssetId, +) -> (T::PoolAssetId, T::AccountId, AccountIdLookupOf) +where + T::AssetBalance: From, + T::Currency: Unbalanced, + T::Assets: Create + Mutate, + T::PoolAssetId: Into, +{ + let (_, _) = create_asset::(asset1); + let (caller, caller_lookup) = create_asset::(asset2); + + assert_ok!(AssetConversion::::create_pool( + SystemOrigin::Signed(caller.clone()).into(), + asset1.clone(), + asset2.clone() + )); + let lp_token = get_lp_token_id::(); + + (lp_token, caller, caller_lookup) +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +benchmarks! { + where_clause { + where + T::AssetBalance: From + Into, + T::Currency: Unbalanced, + T::Balance: From + Into, + T::Assets: Create + Mutate, + T::PoolAssetId: Into, + } + + create_pool { + let asset1 = T::MultiAssetIdConverter::get_native(); + let asset2 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(0_u32)); + let (caller, _) = create_asset::(&asset2); + }: _(SystemOrigin::Signed(caller.clone()), asset1.clone(), asset2.clone()) + verify { + let lp_token = get_lp_token_id::(); + let pool_id = (asset1.clone(), asset2.clone()); + assert_last_event::(Event::PoolCreated { creator: caller.clone(), pool_id, lp_token }.into()); + } + + add_liquidity { + let asset1 = T::MultiAssetIdConverter::get_native(); + let asset2 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(0)); + let (lp_token, caller, _) = create_asset_and_pool::(&asset1, &asset2); + let ed: u128 = T::Currency::minimum_balance().into(); + let add_amount = 1000 + ed; + }: _(SystemOrigin::Signed(caller.clone()), asset1.clone(), asset2.clone(), add_amount.into(), 1000.into(), 0.into(), 0.into(), caller.clone()) + verify { + let pool_id = (asset1.clone(), asset2.clone()); + let lp_minted = AssetConversion::::calc_lp_amount_for_zero_supply(&add_amount.into(), &1000.into()).unwrap().into(); + assert_eq!( + T::PoolAssets::balance(lp_token, &caller), + lp_minted.into() + ); + assert_eq!( + T::Currency::balance(&AssetConversion::::get_pool_account(&pool_id)), + add_amount.into() + ); + assert_eq!( + T::Assets::balance(T::BenchmarkHelper::asset_id(0), &AssetConversion::::get_pool_account(&pool_id)), + 1000.into() + ); + } + + remove_liquidity { + let asset1 = T::MultiAssetIdConverter::get_native(); + let asset2 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(0)); + let (lp_token, caller, _) = create_asset_and_pool::(&asset1, &asset2); + let ed: u128 = T::Currency::minimum_balance().into(); + let add_amount = 100 * ed; + let lp_minted = AssetConversion::::calc_lp_amount_for_zero_supply(&add_amount.into(), &1000.into()).unwrap().into(); + let remove_lp_amount = lp_minted.checked_div(10).unwrap(); + + AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + asset1.clone(), + asset2.clone(), + add_amount.into(), + 1000.into(), + 0.into(), + 0.into(), + caller.clone(), + )?; + let total_supply = >::total_issuance(lp_token.clone()); + }: _(SystemOrigin::Signed(caller.clone()), asset1, asset2, remove_lp_amount.into(), 0.into(), 0.into(), caller.clone()) + verify { + let new_total_supply = >::total_issuance(lp_token.clone()); + assert_eq!( + new_total_supply, + total_supply - remove_lp_amount.into() + ); + } + + swap_exact_tokens_for_tokens { + let asset1 = T::MultiAssetIdConverter::get_native(); + let asset2 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(0)); + let asset3 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(1)); + let asset4 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(2)); + + let (_, caller, _) = create_asset_and_pool::(&asset1, &asset2); + let (_, _) = create_asset::(&asset3); + AssetConversion::::create_pool(SystemOrigin::Signed(caller.clone()).into(), asset2.clone(), asset3.clone())?; + let (_, _) = create_asset::(&asset4); + AssetConversion::::create_pool(SystemOrigin::Signed(caller.clone()).into(), asset3.clone(), asset4.clone())?; + + let path: BoundedVec<_, T::MaxSwapPathLength> = + BoundedVec::try_from(vec![asset1.clone(), asset2.clone(), asset3.clone(), asset4.clone()]).unwrap(); + let ed: u128 = T::Currency::minimum_balance().into(); + let add_amount1 = 100 * ed; + + AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + asset1.clone(), + asset2.clone(), + add_amount1.into(), + 200.into(), + 0.into(), + 0.into(), + caller.clone(), + )?; + AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + asset2.clone(), + asset3.clone(), + 200.into(), + 2000.into(), + 0.into(), + 0.into(), + caller.clone(), + )?; + AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + asset3.clone(), + asset4.clone(), + 2000.into(), + 2000.into(), + 0.into(), + 0.into(), + caller.clone(), + )?; + let asset1_balance = T::Currency::balance(&caller); + }: _(SystemOrigin::Signed(caller.clone()), path.clone(), ed.into(), 1.into(), caller.clone(), false) + verify { + let new_asset1_balance = T::Currency::balance(&caller); + assert_eq!( + new_asset1_balance, + asset1_balance - ed.into() + ); + } + + swap_tokens_for_exact_tokens { + let asset1 = T::MultiAssetIdConverter::get_native(); + let asset2 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(0)); + let asset3 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(1)); + let asset4 = T::MultiAssetIdConverter::into_multiasset_id(&T::BenchmarkHelper::asset_id(2)); + + let (_, caller, _) = create_asset_and_pool::(&asset1, &asset2); + let (_, _) = create_asset::(&asset3); + AssetConversion::::create_pool(SystemOrigin::Signed(caller.clone()).into(), asset2.clone(), asset3.clone())?; + let (_, _) = create_asset::(&asset4); + AssetConversion::::create_pool(SystemOrigin::Signed(caller.clone()).into(), asset3.clone(), asset4.clone())?; + + let path: BoundedVec<_, T::MaxSwapPathLength> = + BoundedVec::try_from(vec![asset1.clone(), asset2.clone(), asset3.clone(), asset4.clone()]).unwrap(); + let ed: u128 = T::Currency::minimum_balance().into(); + let add_amount1 = 1000 + ed; + + AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + asset1.clone(), + asset2.clone(), + add_amount1.into(), + 200.into(), + 0.into(), + 0.into(), + caller.clone(), + )?; + AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + asset2.clone(), + asset3.clone(), + 200.into(), + 2000.into(), + 0.into(), + 0.into(), + caller.clone(), + )?; + AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + asset3.clone(), + asset4.clone(), + 2000.into(), + 2000.into(), + 0.into(), + 0.into(), + caller.clone(), + )?; + let asset4_balance = T::Assets::balance(T::BenchmarkHelper::asset_id(2), &caller); + }: _(SystemOrigin::Signed(caller.clone()), path.clone(), 100.into(), add_amount1.into(), caller.clone(), false) + verify { + let new_asset4_balance = T::Assets::balance(T::BenchmarkHelper::asset_id(2), &caller); + assert_eq!( + new_asset4_balance, + asset4_balance + 100.into() + ); + } + + impl_benchmark_test_suite!(AssetConversion, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/frame/asset-conversion/src/lib.rs b/frame/asset-conversion/src/lib.rs new file mode 100644 index 0000000000000..3db694a88e2f4 --- /dev/null +++ b/frame/asset-conversion/src/lib.rs @@ -0,0 +1,1193 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Substrate Asset Conversion pallet +//! +//! Substrate Asset Conversion pallet based on the [Uniswap V2](https://github.com/Uniswap/v2-core) logic. +//! +//! ## Overview +//! +//! This pallet allows you to: +//! +//! - [create a liquidity pool](`Pallet::create_pool()`) for 2 assets +//! - [provide the liquidity](`Pallet::add_liquidity()`) and receive back an LP token +//! - [exchange the LP token back to assets](`Pallet::remove_liquidity()`) +//! - [swap a specific amount of assets for another](`Pallet::swap_exact_tokens_for_tokens()`) if +//! there is a pool created, or +//! - [swap some assets for a specific amount of +//! another](`Pallet::swap_tokens_for_exact_tokens()`). +//! - [query for an exchange price](`AssetConversionApi::quote_price_exact_tokens_for_tokens`) via +//! a runtime call endpoint +//! - [query the size of a liquidity pool](`AssetConversionApi::get_reserves`) via a runtime api +//! endpoint. +//! +//! The `quote_price_exact_tokens_for_tokens` and `quote_price_tokens_for_exact_tokens` functions +//! both take a path parameter of the route to take. If you want to swap from native asset to +//! non-native asset 1, you would pass in a path of `[DOT, 1]` or `[1, DOT]`. If you want to swap +//! from non-native asset 1 to non-native asset 2, you would pass in a path of `[1, DOT, 2]`. +//! +//! (For an example of configuring this pallet to use `MultiLocation` as an asset id, see the +//! cumulus repo). +//! +//! Here is an example `state_call` that asks for a quote of a pool of native versus asset 1: +//! +//! ```text +//! curl -sS -H "Content-Type: application/json" -d \ +//! '{"id":1, "jsonrpc":"2.0", "method": "state_call", "params": ["AssetConversionApi_quote_price_tokens_for_exact_tokens", "0x0101000000000000000000000011000000000000000000"]}' \ +//! http://localhost:9933/ +//! ``` +//! (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; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +mod types; +pub mod weights; + +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod mock; + +use codec::Codec; +use frame_support::{ + ensure, + traits::tokens::{AssetId, Balance}, +}; +use frame_system::{ensure_signed, pallet_prelude::OriginFor}; +pub use pallet::*; +use sp_arithmetic::traits::Unsigned; +use sp_runtime::{ + traits::{ + CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Ensure, MaybeDisplay, TrailingZeroInput, + Zero, + }, + DispatchError, +}; +use sp_std::vec; +pub use types::*; +pub use weights::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + 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, + }, + BoundedBTreeSet, PalletId, + }; + use sp_arithmetic::Permill; + use sp_runtime::{ + traits::{IntegerSquareRoot, One, Zero}, + Saturating, + }; + use sp_std::prelude::*; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Currency type that this works on. + type Currency: InspectFungible + + MutateFungible; + + /// The `Currency::Balance` type of the native currency. + type Balance: Balance; + + /// The type used to describe the amount of fractions converted into assets. + type AssetBalance: Balance; + + /// A type used for conversions between `Balance` and `AssetBalance`. + type HigherPrecisionBalance: IntegerSquareRoot + + One + + Ensure + + Unsigned + + From + + From + + From + + TryInto + + TryInto; + + /// Identifier for the class of non-native asset. + /// Note: A `From` bound here would prevent `MultiLocation` from being used as an + /// `AssetId`. + type AssetId: AssetId + PartialOrd; + + /// Type that identifies either the native currency or a token class from `Assets`. + type MultiAssetId: AssetId + Ord; + + /// Type to convert an `AssetId` into `MultiAssetId`. + type MultiAssetIdConverter: MultiAssetIdConverter; + + /// `AssetId` to address the lp tokens by. + type PoolAssetId: AssetId + PartialOrd + Incrementable + From; + + /// Registry for the assets. + type Assets: Inspect + + Mutate + + AccountTouch + + ContainsPair; + + /// Registry for the lp tokens. Ideally only this pallet should have create permissions on + /// the assets. + type PoolAssets: Inspect + + Create + + Mutate + + AccountTouch; + + /// A % the liquidity providers will take of every swap. Represents 10ths of a percent. + #[pallet::constant] + type LPFee: Get; + + /// A one-time fee to setup the pool. + #[pallet::constant] + type PoolSetupFee: Get; + + /// An account that receives the pool setup fee. + type PoolSetupFeeReceiver: Get; + + /// A fee to withdraw the liquidity. + #[pallet::constant] + type LiquidityWithdrawalFee: Get; + + /// The minimum LP token amount that could be minted. Ameliorates rounding errors. + #[pallet::constant] + type MintMinLiquidity: Get; + + /// The max number of hops in a swap. + #[pallet::constant] + type MaxSwapPathLength: Get; + + /// The pallet's id, used for deriving its sovereign account ID. + #[pallet::constant] + type PalletId: Get; + + /// A setting to allow creating pools with both non-native assets. + #[pallet::constant] + type AllowMultiAssetPools: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The benchmarks need a way to create asset ids from u32s. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; + } + + /// Map from `PoolAssetId` to `PoolInfo`. This establishes whether a pool has been officially + /// created rather than people sending tokens directly to a pool's public account. + #[pallet::storage] + pub type Pools = + StorageMap<_, Blake2_128Concat, PoolIdOf, PoolInfo, OptionQuery>; + + /// Stores the `PoolAssetId` that is going to be used for the next lp token. + /// This gets incremented whenever a new lp pool is created. + #[pallet::storage] + pub type NextPoolAssetId = StorageValue<_, T::PoolAssetId, OptionQuery>; + + // Pallet's events. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A successful call of the `CretaPool` extrinsic will create this event. + PoolCreated { + /// The account that created the pool. + creator: T::AccountId, + /// The pool id associated with the pool. Note that the order of the assets may not be + /// the same as the order specified in the create pool extrinsic. + pool_id: PoolIdOf, + /// The id of the liquidity tokens that will be minted when assets are added to this + /// pool. + lp_token: T::PoolAssetId, + }, + + /// A successful call of the `AddLiquidity` extrinsic will create this event. + LiquidityAdded { + /// The account that the liquidity was taken from. + who: T::AccountId, + /// The account that the liquidity tokens were minted to. + mint_to: T::AccountId, + /// The pool id of the pool that the liquidity was added to. + pool_id: PoolIdOf, + /// The amount of the first asset that was added to the pool. + amount1_provided: AssetBalanceOf, + /// The amount of the second asset that was added to the pool. + amount2_provided: AssetBalanceOf, + /// The id of the lp token that was minted. + lp_token: T::PoolAssetId, + /// The amount of lp tokens that were minted of that id. + lp_token_minted: AssetBalanceOf, + }, + + /// A successful call of the `RemoveLiquidity` extrinsic will create this event. + LiquidityRemoved { + /// The account that the liquidity tokens were burned from. + who: T::AccountId, + /// The account that the assets were transferred to. + withdraw_to: T::AccountId, + /// The pool id that the liquidity was removed from. + pool_id: PoolIdOf, + /// The amount of the first asset that was removed from the pool. + amount1: AssetBalanceOf, + /// The amount of the second asset that was removed from the pool. + amount2: AssetBalanceOf, + /// The id of the lp token that was burned. + lp_token: T::PoolAssetId, + /// The amount of lp tokens that were burned of that id. + lp_token_burned: AssetBalanceOf, + /// Liquidity withdrawal fee (%). + withdrawal_fee: Permill, + }, + /// Assets have been converted from one to another. Both `SwapExactTokenForToken` + /// and `SwapTokenForExactToken` will generate this event. + SwapExecuted { + /// Which account was the instigator of the swap. + who: T::AccountId, + /// The account that the assets were transferred to. + send_to: T::AccountId, + /// 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: AssetBalanceOf, + /// The amount of the second asset that was received. + amount_out: AssetBalanceOf, + }, + /// An amount has been transferred from one account to another. + Transfer { + /// The account that the assets were transferred from. + from: T::AccountId, + /// The account that the assets were transferred to. + to: T::AccountId, + /// The asset that was transferred. + asset: T::MultiAssetId, + /// The amount of the asset that was transferred. + amount: AssetBalanceOf, + }, + } + + #[pallet::error] + pub enum Error { + /// Provided assets are equal. + EqualAssets, + /// Pool already exists. + PoolExists, + /// Desired amount can't be zero. + WrongDesiredAmount, + /// Provided amount should be greater than or equal to the existential deposit/asset's + /// minimal amount. + AmountLessThanMinimal, + /// Reserve needs to always be greater than or equal to the existential deposit/asset's + /// minimal amount. + ReserveLeftLessThanMinimal, + /// Desired amount can't be equal to the pool reserve. + AmountOutTooHigh, + /// The pool doesn't exist. + PoolNotFound, + /// An overflow happened. + Overflow, + /// The minimal amount requirement for the first token in the pair wasn't met. + AssetOneDepositDidNotMeetMinimum, + /// The minimal amount requirement for the second token in the pair wasn't met. + AssetTwoDepositDidNotMeetMinimum, + /// The minimal amount requirement for the first token in the pair wasn't met. + AssetOneWithdrawalDidNotMeetMinimum, + /// The minimal amount requirement for the second token in the pair wasn't met. + AssetTwoWithdrawalDidNotMeetMinimum, + /// Optimal calculated amount is less than desired. + OptimalAmountLessThanDesired, + /// Insufficient liquidity minted. + InsufficientLiquidityMinted, + /// Requested liquidity can't be zero. + ZeroLiquidity, + /// Amount can't be zero. + ZeroAmount, + /// Insufficient liquidity in the pool. + InsufficientLiquidity, + /// Calculated amount out is less than provided minimum amount. + ProvidedMinimumNotSufficientForSwap, + /// Provided maximum amount is not sufficient for swap. + ProvidedMaximumNotSufficientForSwap, + /// Only pools with native on one side are valid. + PoolMustContainNativeCurrency, + /// The provided path must consists of 2 assets at least. + InvalidPath, + /// It was not possible to calculate path data. + PathError, + /// The provided path must consists of unique assets. + NonUniquePath, + } + + #[pallet::hooks] + impl Hooks for Pallet { + fn integrity_test() { + sp_std::if_std! { + sp_io::TestExternalities::new_empty().execute_with(|| { + // ensure that the `AccountId` is set properly and doesn't generate the same + // pool account for different pool ids. + let native = T::MultiAssetIdConverter::get_native(); + // Decode the asset ids from bytes. + let asset_1 = T::MultiAssetIdConverter::into_multiasset_id(&T::AssetId::decode(&mut vec![0u8, 0, 0, 1].as_slice()).unwrap()); + let asset_2 = T::MultiAssetIdConverter::into_multiasset_id(&T::AssetId::decode(&mut vec![255u8, 255, 255, 255].as_slice()).unwrap()); + assert!(asset_1 != asset_2, "unfortunatly decoded to be the same asset."); + let pool_account_1 = Self::get_pool_account(&(native.clone(), asset_1)); + let pool_account_2 = Self::get_pool_account(&(native, asset_2)); + assert!(sp_std::mem::size_of::() >= sp_std::mem::size_of::()); + assert!( + pool_account_1 != pool_account_2, + "AccountId should be set at least to u128" + ); + }); + } + } + } + + /// Pallet's callable functions. + #[pallet::call] + impl Pallet { + /// Creates an empty liquidity pool and an associated new `lp_token` asset + /// (the id of which is returned in the `Event::PoolCreated` event). + /// + /// Once a pool is created, someone may [`Pallet::add_liquidity`] to it. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::create_pool())] + pub fn create_pool( + origin: OriginFor, + asset1: T::MultiAssetId, + asset2: T::MultiAssetId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + ensure!(asset1 != asset2, Error::::EqualAssets); + + let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); + let (asset1, asset2) = pool_id.clone(); + + if !T::AllowMultiAssetPools::get() && !T::MultiAssetIdConverter::is_native(&asset1) { + Err(Error::::PoolMustContainNativeCurrency)?; + } + + ensure!(!Pools::::contains_key(&pool_id), Error::::PoolExists); + + let pool_account = Self::get_pool_account(&pool_id); + frame_system::Pallet::::inc_providers(&pool_account); + + // pay the setup fee + T::Currency::transfer( + &sender, + &T::PoolSetupFeeReceiver::get(), + T::PoolSetupFee::get(), + Preserve, + )?; + + if let Ok(asset) = T::MultiAssetIdConverter::try_convert(&asset1) { + if !T::Assets::contains(&asset, &pool_account) { + T::Assets::touch(asset, pool_account.clone(), sender.clone())?; + } + } + if let Ok(asset) = T::MultiAssetIdConverter::try_convert(&asset2) { + if !T::Assets::contains(&asset, &pool_account) { + T::Assets::touch(asset, pool_account.clone(), sender.clone())?; + } + } + + let lp_token = NextPoolAssetId::::get().unwrap_or(T::PoolAssetId::initial_value()); + let next_lp_token_id = lp_token.increment(); + NextPoolAssetId::::set(Some(next_lp_token_id)); + + T::PoolAssets::create(lp_token.clone(), pool_account.clone(), false, 1u32.into())?; + T::PoolAssets::touch(lp_token.clone(), pool_account.clone(), sender.clone())?; + + let pool_info = PoolInfo { lp_token: lp_token.clone() }; + Pools::::insert(pool_id.clone(), pool_info); + + Self::deposit_event(Event::PoolCreated { creator: sender, pool_id, lp_token }); + + Ok(()) + } + + /// Provide liquidity into the pool of `asset1` and `asset2`. + /// NOTE: an optimal amount of asset1 and asset2 will be calculated and + /// might be different than the provided `amount1_desired`/`amount2_desired` + /// thus you should provide the min amount you're happy to provide. + /// Params `amount1_min`/`amount2_min` represent that. + /// `mint_to` will be sent the liquidity tokens that represent this share of the pool. + /// + /// Once liquidity is added, someone may successfully call + /// [`Pallet::swap_exact_tokens_for_tokens`] successfully. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::add_liquidity())] + pub fn add_liquidity( + origin: OriginFor, + asset1: T::MultiAssetId, + asset2: T::MultiAssetId, + amount1_desired: AssetBalanceOf, + amount2_desired: AssetBalanceOf, + amount1_min: AssetBalanceOf, + amount2_min: AssetBalanceOf, + mint_to: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + + let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); + // swap params if needed + let (amount1_desired, amount2_desired, amount1_min, amount2_min) = + if pool_id.0 == asset1 { + (amount1_desired, amount2_desired, amount1_min, amount2_min) + } else { + (amount2_desired, amount1_desired, amount2_min, amount1_min) + }; + let (asset1, asset2) = pool_id.clone(); + + ensure!( + amount1_desired > Zero::zero() && amount2_desired > Zero::zero(), + Error::::WrongDesiredAmount + ); + + let maybe_pool = Pools::::get(pool_id.clone()); + let pool = maybe_pool.as_ref().ok_or(Error::::PoolNotFound)?; + + let amount1: AssetBalanceOf; + let amount2: AssetBalanceOf; + let pool_account = Self::get_pool_account(&pool_id); + let reserve1 = Self::get_balance(&pool_account, &asset1)?; + let reserve2 = Self::get_balance(&pool_account, &asset2)?; + + if reserve1.is_zero() || reserve2.is_zero() { + amount1 = amount1_desired; + amount2 = amount2_desired; + } else { + let amount2_optimal = Self::quote(&amount1_desired, &reserve1, &reserve2)?; + + if amount2_optimal <= amount2_desired { + ensure!( + amount2_optimal >= amount2_min, + Error::::AssetTwoDepositDidNotMeetMinimum + ); + amount1 = amount1_desired; + amount2 = amount2_optimal; + } else { + let amount1_optimal = Self::quote(&amount2_desired, &reserve2, &reserve1)?; + ensure!( + amount1_optimal <= amount1_desired, + Error::::OptimalAmountLessThanDesired + ); + ensure!( + amount1_optimal >= amount1_min, + Error::::AssetOneDepositDidNotMeetMinimum + ); + amount1 = amount1_optimal; + amount2 = amount2_desired; + } + } + + Self::validate_minimal_amount(amount1.saturating_add(reserve1), &asset1) + .map_err(|_| Error::::AmountLessThanMinimal)?; + Self::validate_minimal_amount(amount2.saturating_add(reserve2), &asset2) + .map_err(|_| Error::::AmountLessThanMinimal)?; + + Self::transfer(&asset1, &sender, &pool_account, amount1, true)?; + Self::transfer(&asset2, &sender, &pool_account, amount2, true)?; + + let total_supply = T::PoolAssets::total_issuance(pool.lp_token.clone()); + + let lp_token_amount: AssetBalanceOf; + if total_supply.is_zero() { + lp_token_amount = Self::calc_lp_amount_for_zero_supply(&amount1, &amount2)?; + T::PoolAssets::mint_into( + pool.lp_token.clone(), + &pool_account, + T::MintMinLiquidity::get(), + )?; + } else { + let side1 = Self::mul_div(&amount1, &total_supply, &reserve1)?; + let side2 = Self::mul_div(&amount2, &total_supply, &reserve2)?; + lp_token_amount = side1.min(side2); + } + + ensure!( + lp_token_amount > T::MintMinLiquidity::get(), + Error::::InsufficientLiquidityMinted + ); + + T::PoolAssets::mint_into(pool.lp_token.clone(), &mint_to, lp_token_amount)?; + + Self::deposit_event(Event::LiquidityAdded { + who: sender, + mint_to, + pool_id, + amount1_provided: amount1, + amount2_provided: amount2, + lp_token: pool.lp_token.clone(), + lp_token_minted: lp_token_amount, + }); + + Ok(()) + } + + /// Allows you to remove liquidity by providing the `lp_token_burn` tokens that will be + /// burned in the process. With the usage of `amount1_min_receive`/`amount2_min_receive` + /// it's possible to control the min amount of returned tokens you're happy with. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::remove_liquidity())] + pub fn remove_liquidity( + origin: OriginFor, + asset1: T::MultiAssetId, + asset2: T::MultiAssetId, + lp_token_burn: AssetBalanceOf, + amount1_min_receive: AssetBalanceOf, + amount2_min_receive: AssetBalanceOf, + withdraw_to: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + + let pool_id = Self::get_pool_id(asset1.clone(), asset2.clone()); + // swap params if needed + let (amount1_min_receive, amount2_min_receive) = if pool_id.0 == asset1 { + (amount1_min_receive, amount2_min_receive) + } else { + (amount2_min_receive, amount1_min_receive) + }; + let (asset1, asset2) = pool_id.clone(); + + ensure!(lp_token_burn > Zero::zero(), Error::::ZeroLiquidity); + + let maybe_pool = Pools::::get(pool_id.clone()); + let pool = maybe_pool.as_ref().ok_or(Error::::PoolNotFound)?; + + let pool_account = Self::get_pool_account(&pool_id); + let reserve1 = Self::get_balance(&pool_account, &asset1)?; + let reserve2 = Self::get_balance(&pool_account, &asset2)?; + + let total_supply = T::PoolAssets::total_issuance(pool.lp_token.clone()); + let withdrawal_fee_amount = T::LiquidityWithdrawalFee::get() * lp_token_burn; + let lp_redeem_amount = lp_token_burn.saturating_sub(withdrawal_fee_amount); + + let amount1 = Self::mul_div(&lp_redeem_amount, &reserve1, &total_supply)?; + let amount2 = Self::mul_div(&lp_redeem_amount, &reserve2, &total_supply)?; + + ensure!( + !amount1.is_zero() && amount1 >= amount1_min_receive, + Error::::AssetOneWithdrawalDidNotMeetMinimum + ); + ensure!( + !amount2.is_zero() && amount2 >= amount2_min_receive, + Error::::AssetTwoWithdrawalDidNotMeetMinimum + ); + let reserve1_left = reserve1.saturating_sub(amount1); + let reserve2_left = reserve2.saturating_sub(amount2); + Self::validate_minimal_amount(reserve1_left, &asset1) + .map_err(|_| Error::::ReserveLeftLessThanMinimal)?; + Self::validate_minimal_amount(reserve2_left, &asset2) + .map_err(|_| Error::::ReserveLeftLessThanMinimal)?; + + // burn the provided lp token amount that includes the fee + T::PoolAssets::burn_from(pool.lp_token.clone(), &sender, lp_token_burn, Exact, Polite)?; + + Self::transfer(&asset1, &pool_account, &withdraw_to, amount1, false)?; + Self::transfer(&asset2, &pool_account, &withdraw_to, amount2, false)?; + + Self::deposit_event(Event::LiquidityRemoved { + who: sender, + withdraw_to, + pool_id, + amount1, + amount2, + lp_token: pool.lp_token.clone(), + lp_token_burned: lp_token_burn, + withdrawal_fee: T::LiquidityWithdrawalFee::get(), + }); + + Ok(()) + } + + /// Swap the exact amount of `asset1` into `asset2`. + /// `amount_out_min` param allows you to specify the min amount of the `asset2` + /// you're happy to receive. + /// + /// [`AssetConversionApi::quote_price_exact_tokens_for_tokens`] runtime call can be called + /// for a quote. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::swap_exact_tokens_for_tokens())] + pub fn swap_exact_tokens_for_tokens( + origin: OriginFor, + path: BoundedVec, + amount_in: AssetBalanceOf, + amount_out_min: AssetBalanceOf, + send_to: T::AccountId, + 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::deposit_event(Event::SwapExecuted { + who: sender, + send_to, + path, + amount_in, + amount_out, + }); + + Ok(()) + } + + /// Swap any amount of `asset1` to get the exact amount of `asset2`. + /// `amount_in_max` param allows to specify the max amount of the `asset1` + /// you're happy to provide. + /// + /// [`AssetConversionApi::quote_price_tokens_for_exact_tokens`] runtime call can be called + /// for a quote. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::swap_tokens_for_exact_tokens())] + pub fn swap_tokens_for_exact_tokens( + origin: OriginFor, + path: BoundedVec, + amount_out: AssetBalanceOf, + amount_in_max: AssetBalanceOf, + send_to: T::AccountId, + keep_alive: bool, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + + 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); + + Self::do_swap(&sender, &amounts, &path, &send_to, keep_alive)?; + + Self::deposit_event(Event::SwapExecuted { + who: sender, + send_to, + path, + amount_in, + amount_out, + }); + + Ok(()) + } + } + + impl Pallet { + fn transfer( + asset_id: &T::MultiAssetId, + from: &T::AccountId, + to: &T::AccountId, + amount: AssetBalanceOf, + keep_alive: bool, + ) -> Result { + Self::deposit_event(Event::Transfer { + from: from.clone(), + to: to.clone(), + asset: (*asset_id).clone(), + amount, + }); + if T::MultiAssetIdConverter::is_native(asset_id) { + let preservation = match keep_alive { + true => Preserve, + false => Expendable, + }; + let amount = Self::asset_to_native(amount)?; + Ok(Self::native_to_asset(T::Currency::transfer(from, to, amount, preservation)?)?) + } else { + T::Assets::transfer( + T::MultiAssetIdConverter::try_convert(&asset_id) + .map_err(|_| Error::::Overflow)?, + from, + to, + amount, + Expendable, + ) + } + } + + pub(crate) fn native_to_asset(amount: T::Balance) -> Result> { + T::HigherPrecisionBalance::from(amount) + .try_into() + .map_err(|_| Error::::Overflow) + } + + pub(crate) fn asset_to_native(amount: T::AssetBalance) -> Result> { + T::HigherPrecisionBalance::from(amount) + .try_into() + .map_err(|_| Error::::Overflow) + } + + pub(crate) fn do_swap( + sender: &T::AccountId, + amounts: &Vec>, + path: &BoundedVec, + send_to: &T::AccountId, + keep_alive: bool, + ) -> Result<(), DispatchError> { + 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"); + + Self::transfer(asset1, sender, &pool_account, *first_amount, keep_alive)?; + + 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::::PathError)?; + + let to = if i < path_len - 2 { + let asset3 = path.get((i + 2) as usize).ok_or(Error::::PathError)?; + Self::get_pool_account(&Self::get_pool_id( + asset2.clone(), + asset3.clone(), + )) + } else { + send_to.clone() + }; + + 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)?; + + Self::transfer(asset2, &pool_account, &to, *amount_out, true)?; + } + i.saturating_inc(); + } + } + Ok(()) + } + + /// The account ID of the pool. + /// + /// This actually does computation. If you need to keep using it, then make sure you cache + /// the value and only call this once. + pub fn get_pool_account(pool_id: &PoolIdOf) -> T::AccountId { + let encoded_pool_id = sp_io::hashing::blake2_256(&Encode::encode(pool_id)[..]); + + Decode::decode(&mut TrailingZeroInput::new(encoded_pool_id.as_ref())) + .expect("infinite length input; no invalid inputs for type; qed") + } + + fn get_balance( + owner: &T::AccountId, + asset: &T::MultiAssetId, + ) -> Result> { + if T::MultiAssetIdConverter::is_native(asset) { + Self::native_to_asset(<::Currency>::reducible_balance( + owner, Expendable, Polite, + )) + } else { + Ok(<::Assets>::reducible_balance( + T::MultiAssetIdConverter::try_convert(asset) + .map_err(|_| Error::::Overflow)?, + owner, + Expendable, + Polite, + )) + } + } + + /// Returns a pool id constructed from 2 sorted assets. + /// Native asset should be lower than the other asset ids. + pub fn get_pool_id(asset1: T::MultiAssetId, asset2: T::MultiAssetId) -> PoolIdOf { + if asset1 <= asset2 { + (asset1, asset2) + } else { + (asset2, asset1) + } + } + + /// Returns the balance of each asset in the pool. + /// The tuple result is in the order requested (not necessarily the same as pool order). + pub fn get_reserves( + asset1: &T::MultiAssetId, + asset2: &T::MultiAssetId, + ) -> Result<(AssetBalanceOf, AssetBalanceOf), Error> { + 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)?; + let balance2 = Self::get_balance(&pool_account, asset2)?; + + if balance1.is_zero() || balance2.is_zero() { + Err(Error::::PoolNotFound)?; + } + + Ok((balance1, balance2)) + } + + pub(crate) fn get_amounts_in( + amount_out: &AssetBalanceOf, + path: &BoundedVec, + ) -> Result>, DispatchError> { + let mut amounts: Vec> = vec![*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)?; + let prev_amount = amounts.last().expect("Always has at least one element"); + let amount_in = Self::get_amount_in(prev_amount, &reserve_in, &reserve_out)?; + amounts.push(amount_in); + } + } + + amounts.reverse(); + Ok(amounts) + } + + pub(crate) fn get_amounts_out( + amount_in: &AssetBalanceOf, + path: &BoundedVec, + ) -> Result>, DispatchError> { + let mut amounts: Vec> = vec![*amount_in]; + + for assets_pair in path.windows(2) { + if let [asset1, asset2] = assets_pair { + let (reserve_in, reserve_out) = Self::get_reserves(asset1, asset2)?; + let prev_amount = amounts.last().expect("Always has at least one element"); + let amount_out = Self::get_amount_out(prev_amount, &reserve_in, &reserve_out)?; + amounts.push(amount_out); + } + } + + Ok(amounts) + } + + /// Used by the RPC service to provide current prices. + pub fn quote_price_exact_tokens_for_tokens( + asset1: T::MultiAssetId, + asset2: T::MultiAssetId, + amount: AssetBalanceOf, + 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() + } + } else { + None + } + } + + /// Used by the RPC service to provide current prices. + pub fn quote_price_tokens_for_exact_tokens( + asset1: T::MultiAssetId, + asset2: T::MultiAssetId, + amount: AssetBalanceOf, + 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() + } + } else { + None + } + } + + /// Calculates the optimal amount from the reserves. + pub fn quote( + amount: &AssetBalanceOf, + reserve1: &AssetBalanceOf, + reserve2: &AssetBalanceOf, + ) -> Result, Error> { + // amount * reserve2 / reserve1 + Self::mul_div(amount, reserve2, reserve1) + } + + pub(super) fn calc_lp_amount_for_zero_supply( + amount1: &AssetBalanceOf, + amount2: &AssetBalanceOf, + ) -> Result, Error> { + let amount1 = T::HigherPrecisionBalance::from(*amount1); + let amount2 = T::HigherPrecisionBalance::from(*amount2); + + let result = amount1 + .checked_mul(&amount2) + .ok_or(Error::::Overflow)? + .integer_sqrt() + .checked_sub(&T::MintMinLiquidity::get().into()) + .ok_or(Error::::InsufficientLiquidityMinted)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + fn mul_div( + a: &AssetBalanceOf, + b: &AssetBalanceOf, + c: &AssetBalanceOf, + ) -> Result, Error> { + let a = T::HigherPrecisionBalance::from(*a); + let b = T::HigherPrecisionBalance::from(*b); + let c = T::HigherPrecisionBalance::from(*c); + + let result = a + .checked_mul(&b) + .ok_or(Error::::Overflow)? + .checked_div(&c) + .ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + /// Calculates amount out + /// + /// Given an input amount of an asset and pair reserves, returns the maximum output amount + /// of the other asset + pub fn get_amount_out( + amount_in: &AssetBalanceOf, + reserve_in: &AssetBalanceOf, + reserve_out: &AssetBalanceOf, + ) -> Result, Error> { + let amount_in = T::HigherPrecisionBalance::from(*amount_in); + let reserve_in = T::HigherPrecisionBalance::from(*reserve_in); + let reserve_out = T::HigherPrecisionBalance::from(*reserve_out); + + if reserve_in.is_zero() || reserve_out.is_zero() { + return Err(Error::::ZeroLiquidity.into()) + } + + let amount_in_with_fee = amount_in + .checked_mul(&(T::HigherPrecisionBalance::from(1000u32) - (T::LPFee::get().into()))) + .ok_or(Error::::Overflow)?; + + let numerator = + amount_in_with_fee.checked_mul(&reserve_out).ok_or(Error::::Overflow)?; + + let denominator = reserve_in + .checked_mul(&1000u32.into()) + .ok_or(Error::::Overflow)? + .checked_add(&amount_in_with_fee) + .ok_or(Error::::Overflow)?; + + let result = numerator.checked_div(&denominator).ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + /// Calculates amount in + /// + /// Given an output amount of an asset and pair reserves, returns a required input amount + /// of the other asset + pub fn get_amount_in( + amount_out: &AssetBalanceOf, + reserve_in: &AssetBalanceOf, + reserve_out: &AssetBalanceOf, + ) -> Result, Error> { + let amount_out = T::HigherPrecisionBalance::from(*amount_out); + let reserve_in = T::HigherPrecisionBalance::from(*reserve_in); + let reserve_out = T::HigherPrecisionBalance::from(*reserve_out); + + if reserve_in.is_zero() || reserve_out.is_zero() { + Err(Error::::ZeroLiquidity.into())? + } + + if amount_out >= reserve_out { + Err(Error::::AmountOutTooHigh.into())? + } + + let numerator = reserve_in + .checked_mul(&amount_out) + .ok_or(Error::::Overflow)? + .checked_mul(&1000u32.into()) + .ok_or(Error::::Overflow)?; + + let denominator = reserve_out + .checked_sub(&amount_out) + .ok_or(Error::::Overflow)? + .checked_mul(&(T::HigherPrecisionBalance::from(1000u32) - T::LPFee::get().into())) + .ok_or(Error::::Overflow)?; + + let result = numerator + .checked_div(&denominator) + .ok_or(Error::::Overflow)? + .checked_add(&One::one()) + .ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + fn validate_minimal_amount( + value: T::AssetBalance, + asset: &T::MultiAssetId, + ) -> Result<(), ()> { + if T::MultiAssetIdConverter::is_native(asset) { + let ed = T::Currency::minimum_balance(); + ensure!( + T::HigherPrecisionBalance::from(value) >= T::HigherPrecisionBalance::from(ed), + () + ); + } else { + let asset_id = T::MultiAssetIdConverter::try_convert(asset).map_err(|_| ())?; + let minimal = T::Assets::minimum_balance(asset_id); + ensure!(value >= minimal, ()); + } + Ok(()) + } + + fn validate_swap_path( + path: &BoundedVec, + ) -> Result<(), DispatchError> { + ensure!(path.len() >= 2, Error::::InvalidPath); + + // validate all the pools in the path are unique + let mut pools = BoundedBTreeSet::, T::MaxSwapPathLength>::new(); + 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 new_element = pools.try_insert(pool_id).expect("can't get here"); + if !new_element { + return Err(Error::::NonUniquePath.into()) + } + } + } + Ok(()) + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + /// Returns the next pool asset id for benchmark purposes only. + pub fn get_next_pool_asset_id() -> T::PoolAssetId { + NextPoolAssetId::::get().unwrap_or(T::PoolAssetId::initial_value()) + } + } +} + +impl + frame_support::traits::tokens::fungibles::SwapForNative< + T::RuntimeOrigin, + T::AccountId, + T::Balance, + T::AssetBalance, + T::AssetId, + > for Pallet +where + ::Currency: + frame_support::traits::tokens::fungible::Inspect<::AccountId>, +{ + // If successful returns the amount in. + fn swap_tokens_for_exact_native( + sender: T::AccountId, + asset_id: T::AssetId, + amount_out: T::Balance, + 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); + } + 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(); + + let amount_out = Self::native_to_asset(amount_out)?; + + let amounts = Self::get_amounts_in(&amount_out, &path)?; + let amount_in = *amounts.first().expect("Always has more than one element"); + 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)?; + + Self::deposit_event(Event::SwapExecuted { + who: sender, + send_to, + path, + amount_in, + amount_out, + }); + + Ok(amount_in) + } +} + +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 + Balance: Codec + MaybeDisplay, + AssetBalance: frame_support::traits::tokens::Balance, + AssetId: Codec + { + /// Provides a quote for [`Pallet::swap_tokens_for_exact_tokens`]. + /// + /// 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; + + /// 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; + + /// 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/frame/asset-conversion/src/mock.rs b/frame/asset-conversion/src/mock.rs new file mode 100644 index 0000000000000..34d2eeb273ca8 --- /dev/null +++ b/frame/asset-conversion/src/mock.rs @@ -0,0 +1,197 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test environment for Asset Conversion pallet. + +use super::*; +use crate as pallet_asset_conversion; + +use frame_support::{ + construct_runtime, + instances::{Instance1, Instance2}, + ord_parameter_types, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64}, + PalletId, +}; +use frame_system::{EnsureSigned, EnsureSignedBy}; +use sp_arithmetic::Permill; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Assets: pallet_assets::, + PoolAssets: pallet_assets::, + AssetConversion: pallet_asset_conversion, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u128; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type Balance = u128; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<100>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); + type MaxHolds = (); +} + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = u128; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<10>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type Extra = (); + type WeightInfo = (); + type CallbackHandle = (); + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = u128; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = + AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU128<0>; + type AssetAccountDeposit = ConstU128<0>; + type MetadataDepositBase = ConstU128<0>; + type MetadataDepositPerByte = ConstU128<0>; + type ApprovalDeposit = ConstU128<0>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type Extra = (); + type WeightInfo = (); + type CallbackHandle = (); + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +parameter_types! { + pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon"); + pub storage AllowMultiAssetPools: bool = true; + pub storage LiquidityWithdrawalFee: Permill = Permill::from_percent(0); // should be non-zero if AllowMultiAssetPools is true, otherwise can be zero +} + +ord_parameter_types! { + pub const AssetConversionOrigin: u128 = AccountIdConversion::::into_account_truncating(&AssetConversionPalletId::get()); +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type AssetBalance = ::Balance; + type AssetId = u32; + type PoolAssetId = u32; + type Assets = Assets; + type PoolAssets = PoolAssets; + type PalletId = AssetConversionPalletId; + type WeightInfo = (); + type LPFee = ConstU32<3>; // means 0.3% + type PoolSetupFee = ConstU128<100>; // should be more or equal to the existential deposit + type PoolSetupFeeReceiver = AssetConversionOrigin; + type LiquidityWithdrawalFee = LiquidityWithdrawalFee; + type AllowMultiAssetPools = AllowMultiAssetPools; + type MaxSwapPathLength = ConstU32<4>; + type MintMinLiquidity = ConstU128<100>; // 100 is good enough when the main currency has 12 decimals. + + type Balance = u128; + type HigherPrecisionBalance = sp_core::U256; + + type MultiAssetId = NativeOrAssetId; + type MultiAssetIdConverter = NativeOrAssetIdConverter; + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![(1, 10000), (2, 20000), (3, 30000), (4, 40000)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/frame/asset-conversion/src/tests.rs b/frame/asset-conversion/src/tests.rs new file mode 100644 index 0000000000000..4b166c5cb44c5 --- /dev/null +++ b/frame/asset-conversion/src/tests.rs @@ -0,0 +1,1413 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{mock::*, *}; +use frame_support::{ + assert_noop, assert_ok, + instances::Instance1, + traits::{fungible::Inspect, fungibles::InspectEnumerable, Get}, +}; +use sp_arithmetic::Permill; +use sp_runtime::{DispatchError, TokenError}; + +fn events() -> Vec> { + let result = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let mock::RuntimeEvent::AssetConversion(inner) = e { + Some(inner) + } else { + None + } + }) + .collect(); + + System::reset_events(); + + result +} + +fn pools() -> Vec> { + let mut s: Vec<_> = Pools::::iter().map(|x| x.0).collect(); + s.sort(); + s +} + +fn assets() -> Vec> { + // if the storage would be public: + // let mut s: Vec<_> = pallet_assets::pallet::Asset::::iter().map(|x| x.0).collect(); + let mut s: Vec<_> = <::Assets>::asset_ids() + .map(|id| NativeOrAssetId::Asset(id)) + .collect(); + s.sort(); + s +} + +fn pool_assets() -> Vec { + let mut s: Vec<_> = <::PoolAssets>::asset_ids().collect(); + s.sort(); + s +} + +fn create_tokens(owner: u128, tokens: Vec>) { + for token_id in tokens { + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + NativeOrAssetIdConverter::try_convert(&token_id).unwrap(), + owner, + false, + 1 + )); + } +} + +fn balance(owner: u128, token_id: NativeOrAssetId) -> u128 { + match token_id { + NativeOrAssetId::Native => <::Currency>::free_balance(owner), + NativeOrAssetId::Asset(token_id) => <::Assets>::balance(token_id, owner), + } +} + +fn pool_balance(owner: u128, token_id: u32) -> u128 { + <::PoolAssets>::balance(token_id, owner) +} + +fn get_ed() -> u128 { + <::Currency>::minimum_balance() +} + +macro_rules! bvec { + ($( $x:tt )*) => { + vec![$( $x )*].try_into().unwrap() + } +} + +#[test] +fn check_pool_accounts_dont_collide() { + use std::collections::HashSet; + let mut map = HashSet::new(); + + for i in 0..1_000_000u32 { + let account = AssetConversion::get_pool_account(&( + NativeOrAssetId::Native, + NativeOrAssetId::Asset(i), + )); + if map.contains(&account) { + panic!("Collision at {}", i); + } + map.insert(account); + } +} + +#[test] +fn check_max_numbers() { + new_test_ext().execute_with(|| { + assert_eq!(AssetConversion::quote(&3u128, &u128::MAX, &u128::MAX).ok().unwrap(), 3); + assert!(AssetConversion::quote(&u128::MAX, &3u128, &u128::MAX).is_err()); + assert_eq!(AssetConversion::quote(&u128::MAX, &u128::MAX, &1u128).ok().unwrap(), 1); + + assert_eq!( + AssetConversion::get_amount_out(&100u128, &u128::MAX, &u128::MAX).ok().unwrap(), + 99 + ); + assert_eq!( + AssetConversion::get_amount_in(&100u128, &u128::MAX, &u128::MAX).ok().unwrap(), + 101 + ); + }); +} + +#[test] +fn can_create_pool() { + new_test_ext().execute_with(|| { + let asset_account_deposit: u128 = + >::AssetAccountDeposit::get(); + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + let pool_id = (token_1, token_2); + + create_tokens(user, vec![token_2]); + + let lp_token = AssetConversion::get_next_pool_asset_id(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 1000)); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_2, token_1)); + + let setup_fee = <::PoolSetupFee as Get<::Balance>>::get(); + let pool_account = <::PoolSetupFeeReceiver as Get>::get(); + assert_eq!( + balance(user, NativeOrAssetId::Native), + 1000 - (setup_fee + asset_account_deposit) + ); + assert_eq!(balance(pool_account, NativeOrAssetId::Native), setup_fee); + assert_eq!(lp_token + 1, AssetConversion::get_next_pool_asset_id()); + + assert_eq!(events(), [Event::::PoolCreated { creator: user, pool_id, lp_token }]); + assert_eq!(pools(), vec![pool_id]); + assert_eq!(assets(), vec![token_2]); + assert_eq!(pool_assets(), vec![lp_token]); + + assert_noop!( + AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_1), + Error::::EqualAssets + ); + assert_noop!( + AssetConversion::create_pool(RuntimeOrigin::signed(user), token_2, token_2), + Error::::EqualAssets + ); + + // validate we can create Asset(1)/Asset(2) pool + let token_1 = NativeOrAssetId::Asset(1); + create_tokens(user, vec![token_1]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + // validate we can force the first asset to be the Native currency only + AllowMultiAssetPools::set(&false); + let token_1 = NativeOrAssetId::Asset(3); + assert_noop!( + AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2), + Error::::PoolMustContainNativeCurrency + ); + }); +} + +#[test] +fn create_same_pool_twice_should_fail() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + + create_tokens(user, vec![token_2]); + + let lp_token = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_2, token_1)); + let expected_free = lp_token + 1; + assert_eq!(expected_free, AssetConversion::get_next_pool_asset_id()); + + assert_noop!( + AssetConversion::create_pool(RuntimeOrigin::signed(user), token_2, token_1), + Error::::PoolExists + ); + assert_eq!(expected_free, AssetConversion::get_next_pool_asset_id()); + + // Try switching the same tokens around: + assert_noop!( + AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2), + Error::::PoolExists + ); + assert_eq!(expected_free, AssetConversion::get_next_pool_asset_id()); + }); +} + +#[test] +fn different_pools_should_have_different_lp_tokens() { + 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); + let pool_id_1_2 = (token_1, token_2); + let pool_id_1_3 = (token_1, token_3); + + create_tokens(user, vec![token_2, token_3]); + + let lp_token2_1 = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_2, token_1)); + let lp_token3_1 = AssetConversion::get_next_pool_asset_id(); + + assert_eq!( + events(), + [Event::::PoolCreated { + creator: user, + pool_id: pool_id_1_2, + lp_token: lp_token2_1 + }] + ); + + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_3, token_1)); + assert_eq!( + events(), + [Event::::PoolCreated { + creator: user, + pool_id: pool_id_1_3, + lp_token: lp_token3_1, + }] + ); + + assert_ne!(lp_token2_1, lp_token3_1); + }); +} + +#[test] +fn can_add_liquidity() { + 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]); + let lp_token1 = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + let lp_token2 = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_3)); + + let ed = get_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 * 2 + ed)); + 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, + 10, + 10000, + 10, + user, + )); + + let pool_id = (token_1, token_2); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id, + amount1_provided: 10000, + amount2_provided: 10, + lp_token: lp_token1, + lp_token_minted: 216, + })); + let pallet_account = AssetConversion::get_pool_account(&pool_id); + assert_eq!(balance(pallet_account, token_1), 10000); + assert_eq!(balance(pallet_account, token_2), 10); + assert_eq!(balance(user, token_1), 10000 + ed); + assert_eq!(balance(user, token_2), 1000 - 10); + assert_eq!(pool_balance(user, lp_token1), 216); + + // try to pass the non-native - native assets, the result should be the same + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_3, + token_1, + 10, + 10000, + 10, + 10000, + user, + )); + + let pool_id = (token_1, token_3); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id, + amount1_provided: 10000, + amount2_provided: 10, + lp_token: lp_token2, + lp_token_minted: 216, + })); + let pallet_account = AssetConversion::get_pool_account(&pool_id); + assert_eq!(balance(pallet_account, token_1), 10000); + assert_eq!(balance(pallet_account, token_3), 10); + assert_eq!(balance(user, token_1), ed); + assert_eq!(balance(user, token_3), 1000 - 10); + assert_eq!(pool_balance(user, lp_token2), 216); + }); +} + +#[test] +fn add_tiny_liquidity_leads_to_insufficient_liquidity_minted_error() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_noop!( + AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 1, + 1, + 1, + 1, + user + ), + Error::::AmountLessThanMinimal + ); + + assert_noop!( + AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + get_ed(), + 1, + 1, + 1, + user + ), + Error::::InsufficientLiquidityMinted + ); + }); +} + +#[test] +fn add_tiny_liquidity_directly_to_pool_address() { + 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)); + + let ed = get_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 * 2 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, 1000)); + + // check we're still able to add the liquidity even when the pool already has some token_1 + let pallet_account = AssetConversion::get_pool_account(&(token_1, token_2)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), pallet_account, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 10, + 10000, + 10, + user, + )); + + // check the same but for token_3 (non-native token) + let pallet_account = AssetConversion::get_pool_account(&(token_1, token_3)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, pallet_account, 1)); + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_3, + 10000, + 10, + 10000, + 10, + user, + )); + }); +} + +#[test] +fn can_remove_liquidity() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + let pool_id = (token_1, token_2); + + create_tokens(user, vec![token_2]); + let lp_token = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000000000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 100000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 1000000000, + 100000, + 1000000000, + 100000, + user, + )); + + let total_lp_received = pool_balance(user, lp_token); + LiquidityWithdrawalFee::set(&Permill::from_percent(10)); + + assert_ok!(AssetConversion::remove_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + total_lp_received, + 0, + 0, + user, + )); + + assert!(events().contains(&Event::::LiquidityRemoved { + who: user, + withdraw_to: user, + pool_id, + amount1: 899991000, + amount2: 89999, + lp_token, + lp_token_burned: total_lp_received, + withdrawal_fee: ::LiquidityWithdrawalFee::get() + })); + + let pool_account = AssetConversion::get_pool_account(&pool_id); + assert_eq!(balance(pool_account, token_1), 100009000); + assert_eq!(balance(pool_account, token_2), 10001); + assert_eq!(pool_balance(pool_account, lp_token), 100); + + assert_eq!(balance(user, token_1), 10000000000 - 1000000000 + 899991000); + assert_eq!(balance(user, token_2), 89999); + assert_eq!(pool_balance(user, lp_token), 0); + }); +} + +#[test] +fn can_not_redeem_more_lp_tokens_than_were_minted() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + let lp_token = AssetConversion::get_next_pool_asset_id(); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 + get_ed())); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 10, + 10000, + 10, + user, + )); + + // Only 216 lp_tokens_minted + assert_eq!(pool_balance(user, lp_token), 216); + + assert_noop!( + AssetConversion::remove_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 216 + 1, // Try and redeem 10 lp tokens while only 9 minted. + 0, + 0, + user, + ), + DispatchError::Token(TokenError::FundsUnavailable) + ); + }); +} + +#[test] +fn can_quote_price() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 100000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 200, + 1, + 1, + user, + )); + + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrAssetId::Native, + NativeOrAssetId::Asset(2), + 3000, + false, + ), + Some(60) + ); + // 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_exact_tokens_for_tokens( + NativeOrAssetId::Native, + NativeOrAssetId::Asset(2), + 3000, + false, + ), + Some(60) + ); + + // Check inverse: + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrAssetId::Asset(2), + NativeOrAssetId::Native, + 60, + false, + ), + Some(3000) + ); + }); +} + +#[test] +fn can_swap_with_native() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + let pool_id = (token_1, token_2); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + let ed = get_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let input_amount = 100; + let expect_receive = + AssetConversion::get_amount_out(&input_amount, &liquidity2, &liquidity1) + .ok() + .unwrap(); + + assert_ok!(AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + input_amount, + 1, + user, + false, + )); + + let pallet_account = AssetConversion::get_pool_account(&pool_id); + assert_eq!(balance(user, token_1), expect_receive + ed); + assert_eq!(balance(user, token_2), 1000 - liquidity2 - input_amount); + assert_eq!(balance(pallet_account, token_1), liquidity1 - expect_receive); + assert_eq!(balance(pallet_account, token_2), liquidity2 + input_amount); + }); +} + +#[test] +fn can_swap_with_realistic_values() { + new_test_ext().execute_with(|| { + let user = 1; + let dot = NativeOrAssetId::Native; + let usd = NativeOrAssetId::Asset(2); + create_tokens(user, vec![usd]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), dot, usd)); + + const UNIT: u128 = 1_000_000_000; + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 300_000 * UNIT)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1_100_000 * UNIT)); + + let liquidity_dot = 200_000 * UNIT; // ratio for a 5$ price + let liquidity_usd = 1_000_000 * UNIT; + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + dot, + usd, + liquidity_dot, + liquidity_usd, + 1, + 1, + user, + )); + + let input_amount = 10 * UNIT; // usd + + assert_ok!(AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![usd, dot], + input_amount, + 1, + user, + false, + )); + + assert!(events().contains(&Event::::SwapExecuted { + who: user, + send_to: user, + path: bvec![usd, dot], + amount_in: 10 * UNIT, // usd + amount_out: 1_993_980_120, // About 2 dot after div by UNIT. + })); + }); +} + +#[test] +fn can_not_swap_in_pool_with_no_liquidity_added_yet() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + // Check can't swap an empty pool + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + 10, + 1, + user, + false, + ), + Error::::PoolNotFound + ); + }); +} + +#[test] +fn check_no_panic_when_try_swap_close_to_empty_pool() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + let pool_id = (token_1, token_2); + let lp_token = AssetConversion::get_next_pool_asset_id(); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + let ed = get_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let lp_token_minted = pool_balance(user, lp_token); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id, + amount1_provided: liquidity1, + amount2_provided: liquidity2, + lp_token, + lp_token_minted, + })); + + let pallet_account = AssetConversion::get_pool_account(&pool_id); + assert_eq!(balance(pallet_account, token_1), liquidity1); + assert_eq!(balance(pallet_account, token_2), liquidity2); + + assert_ok!(AssetConversion::remove_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + lp_token_minted, + 1, + 1, + user, + )); + + // Now, the pool should exist but be almost empty. + // Let's try and drain it. + assert_eq!(balance(pallet_account, token_1), 708); + assert_eq!(balance(pallet_account, token_2), 15); + + // validate the reserve should always stay above the ED + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + 708 - ed + 1, // amount_out + 500, // amount_in_max + user, + false, + ), + Error::::ReserveLeftLessThanMinimal + ); + + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + 608, // amount_out + 500, // amount_in_max + user, + false, + )); + + let token_1_left = balance(pallet_account, token_1); + let token_2_left = balance(pallet_account, token_2); + assert_eq!(token_1_left, 708 - 608); + + // The price for the last tokens should be very high + assert_eq!( + AssetConversion::get_amount_in(&(token_1_left - 1), &token_2_left, &token_1_left) + .ok() + .unwrap(), + 10625 + ); + + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + token_1_left - 1, // amount_out + 1000, // amount_in_max + user, + false, + ), + Error::::ProvidedMaximumNotSufficientForSwap + ); + + // Try to swap what's left in the pool + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + token_1_left, // amount_out + 1000, // amount_in_max + user, + false, + ), + Error::::AmountOutTooHigh + ); + }); +} + +#[test] +fn swap_should_not_work_if_too_much_slippage() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 + get_ed())); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_amount = 100; + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + exchange_amount, // amount_in + 4000, // amount_out_min + user, + false, + ), + Error::::ProvidedMinimumNotSufficientForSwap + ); + }); +} + +#[test] +fn can_swap_tokens_for_exact_tokens() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + let pool_id = (token_1, token_2); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + let ed = get_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let pallet_account = AssetConversion::get_pool_account(&pool_id); + let before1 = balance(pallet_account, token_1) + balance(user, token_1); + let before2 = balance(pallet_account, token_2) + balance(user, token_2); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_out = 50; + let expect_in = AssetConversion::get_amount_in(&exchange_out, &liquidity1, &liquidity2) + .ok() + .unwrap(); + + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2], + exchange_out, // amount_out + 3500, // amount_in_max + user, + true, + )); + + assert_eq!(balance(user, token_1), 10000 + ed - expect_in); + assert_eq!(balance(user, token_2), 1000 - liquidity2 + exchange_out); + assert_eq!(balance(pallet_account, token_1), liquidity1 + expect_in); + assert_eq!(balance(pallet_account, token_2), liquidity2 - exchange_out); + + // check invariants: + + // native and asset totals should be preserved. + assert_eq!(before1, balance(pallet_account, token_1) + balance(user, token_1)); + assert_eq!(before2, balance(pallet_account, token_2) + balance(user, token_2)); + }); +} + +#[test] +fn can_swap_tokens_for_exact_tokens_when_not_liquidity_provider() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + let pool_id = (token_1, token_2); + let lp_token = AssetConversion::get_next_pool_asset_id(); + + create_tokens(user2, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user2), token_1, token_2)); + + let ed = get_ed(); + let base1 = 10000; + let base2 = 1000; + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, base1 + ed)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, base1 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user2, base2)); + + let pallet_account = AssetConversion::get_pool_account(&pool_id); + let before1 = + balance(pallet_account, token_1) + balance(user, token_1) + balance(user2, token_1); + let before2 = + balance(pallet_account, token_2) + balance(user, token_2) + balance(user2, token_2); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user2), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user2, + )); + + assert_eq!(balance(user, token_1), base1 + ed); + assert_eq!(balance(user, token_2), 0); + + let exchange_out = 50; + let expect_in = AssetConversion::get_amount_in(&exchange_out, &liquidity1, &liquidity2) + .ok() + .unwrap(); + + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2], + exchange_out, // amount_out + 3500, // amount_in_max + user, + true, + )); + + assert_eq!(balance(user, token_1), base1 + ed - expect_in); + assert_eq!(balance(pallet_account, token_1), liquidity1 + expect_in); + assert_eq!(balance(user, token_2), exchange_out); + assert_eq!(balance(pallet_account, token_2), liquidity2 - exchange_out); + + // check invariants: + + // native and asset totals should be preserved. + assert_eq!( + before1, + balance(pallet_account, token_1) + balance(user, token_1) + balance(user2, token_1) + ); + assert_eq!( + before2, + balance(pallet_account, token_2) + balance(user, token_2) + balance(user2, token_2) + ); + + let lp_token_minted = pool_balance(user2, lp_token); + assert_eq!(lp_token_minted, 1314); + + assert_ok!(AssetConversion::remove_liquidity( + RuntimeOrigin::signed(user2), + token_1, + token_2, + lp_token_minted, + 0, + 0, + user2, + )); + }); +} + +#[test] +fn swap_when_existential_deposit_would_cause_reaping_but_keep_alive_set() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + + create_tokens(user2, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user2), token_1, token_2)); + + let ed = get_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 101)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 10000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user2, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user2), + token_1, + token_2, + 10000, + 200, + 1, + 1, + user2, + )); + + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2], + 1, // amount_out + 101, // amount_in_max + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2], + 51, // amount_in + 1, // amount_out_min + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + }); +} + +#[test] +fn swap_tokens_for_exact_tokens_should_not_work_if_too_much_slippage() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + get_ed())); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_out = 1; + + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2], + exchange_out, // amount_out + 50, // amount_in_max just greater than slippage. + user, + true + ), + Error::::ProvidedMaximumNotSufficientForSwap + ); + }); +} + +#[test] +fn swap_exact_tokens_for_tokens_in_multi_hops() { + 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_2, token_3)); + + let ed = get_ed(); + let base1 = 10000; + let base2 = 10000; + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, base1 * 2 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, base2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, base2)); + + let liquidity1 = 10000; + let liquidity2 = 200; + let liquidity3 = 2000; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_2, + token_3, + liquidity2, + liquidity3, + 1, + 1, + user, + )); + + let input_amount = 500; + let expect_out2 = AssetConversion::get_amount_out(&input_amount, &liquidity1, &liquidity2) + .ok() + .unwrap(); + let expect_out3 = AssetConversion::get_amount_out(&expect_out2, &liquidity2, &liquidity3) + .ok() + .unwrap(); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1], + input_amount, + 80, + user, + true, + ), + Error::::InvalidPath + ); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2, token_3, token_2], + input_amount, + 80, + user, + true, + ), + Error::::NonUniquePath + ); + + assert_ok!(AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2, token_3], + input_amount, // amount_in + 80, // amount_out_min + user, + true, + )); + + let pool_id1 = (token_1, token_2); + let pool_id2 = (token_2, token_3); + let pallet_account1 = AssetConversion::get_pool_account(&pool_id1); + let pallet_account2 = AssetConversion::get_pool_account(&pool_id2); + + assert_eq!(balance(user, token_1), base1 + ed - input_amount); + assert_eq!(balance(pallet_account1, token_1), liquidity1 + input_amount); + assert_eq!(balance(pallet_account1, token_2), liquidity2 - expect_out2); + assert_eq!(balance(pallet_account2, token_2), liquidity2 + expect_out2); + assert_eq!(balance(pallet_account2, token_3), liquidity3 - expect_out3); + assert_eq!(balance(user, token_3), 10000 - liquidity3 + expect_out3); + }); +} + +#[test] +fn swap_tokens_for_exact_tokens_in_multi_hops() { + 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_2, token_3)); + + let ed = get_ed(); + let base1 = 10000; + let base2 = 10000; + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, base1 * 2 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, base2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, base2)); + + let liquidity1 = 10000; + let liquidity2 = 200; + let liquidity3 = 2000; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_2, + token_3, + liquidity2, + liquidity3, + 1, + 1, + user, + )); + + let exchange_out3 = 100; + let expect_in2 = AssetConversion::get_amount_in(&exchange_out3, &liquidity2, &liquidity3) + .ok() + .unwrap(); + let expect_in1 = AssetConversion::get_amount_in(&expect_in2, &liquidity1, &liquidity2) + .ok() + .unwrap(); + + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2, token_3], + exchange_out3, // amount_out + 1000, // amount_in_max + user, + true, + )); + + let pool_id1 = (token_1, token_2); + let pool_id2 = (token_2, token_3); + let pallet_account1 = AssetConversion::get_pool_account(&pool_id1); + let pallet_account2 = AssetConversion::get_pool_account(&pool_id2); + + assert_eq!(balance(user, token_1), base1 + ed - expect_in1); + assert_eq!(balance(pallet_account1, token_1), liquidity1 + expect_in1); + assert_eq!(balance(pallet_account1, token_2), liquidity2 - expect_in2); + assert_eq!(balance(pallet_account2, token_2), liquidity2 + expect_in2); + assert_eq!(balance(pallet_account2, token_3), liquidity3 - exchange_out3); + assert_eq!(balance(user, token_3), 10000 - liquidity3 + exchange_out3); + }); +} + +#[test] +fn can_not_swap_same_asset() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrAssetId::Asset(1); + + create_tokens(user, vec![token_1]); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 1, user, 1000)); + + let liquidity1 = 1000; + let liquidity2 = 20; + assert_noop!( + AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_1, + liquidity1, + liquidity2, + 1, + 1, + user, + ), + Error::::PoolNotFound + ); + + let exchange_amount = 10; + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_1], + exchange_amount, + 1, + user, + true, + ), + Error::::PoolNotFound + ); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![NativeOrAssetId::Native, NativeOrAssetId::Native], + exchange_amount, + 1, + user, + true, + ), + Error::::PoolNotFound + ); + }); +} + +#[test] +fn validate_pool_id_sorting() { + new_test_ext().execute_with(|| { + use crate::NativeOrAssetId::{Asset, Native}; + assert_eq!(AssetConversion::get_pool_id(Native, Asset(2)), (Native, Asset(2))); + assert_eq!(AssetConversion::get_pool_id(Asset(2), Native), (Native, Asset(2))); + assert_eq!(AssetConversion::get_pool_id(Native, Native), (Native, Native)); + assert_eq!(AssetConversion::get_pool_id(Asset(2), Asset(1)), (Asset(1), Asset(2))); + assert!(Asset(2) > Asset(1)); + assert!(Asset(1) <= Asset(1)); + assert_eq!(Asset(1), Asset(1)); + assert_eq!(Native::, Native::); + assert!(Native < Asset(1)); + }); +} + +#[test] +fn cannot_block_pool_creation() { + new_test_ext().execute_with(|| { + // User 1 is the pool creator + let user = 1; + // User 2 is the attacker + let attacker = 2; + + let ed = get_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), attacker, 10000 + ed)); + + // The target pool the user wants to create is Native <=> Asset(2) + let token_1 = NativeOrAssetId::Native; + let token_2 = NativeOrAssetId::Asset(2); + + // Attacker computes the still non-existing pool account for the target pair + let pool_account = + AssetConversion::get_pool_account(&AssetConversion::get_pool_id(token_2, token_1)); + // And transfers the ED to that pool account + assert_ok!(Balances::transfer(RuntimeOrigin::signed(attacker), pool_account, ed)); + // Then, the attacker creates 14 tokens and sends one of each to the pool account + for i in 10..25 { + create_tokens(attacker, vec![NativeOrAssetId::Asset(i)]); + assert_ok!(Assets::mint(RuntimeOrigin::signed(attacker), i, attacker, 1000)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(attacker), i, pool_account, 1)); + } + + // User can still create the pool + create_tokens(user, vec![token_2]); + assert_ok!(AssetConversion::create_pool(RuntimeOrigin::signed(user), token_1, token_2)); + + // User has to transfer one Asset(2) token to the pool account (otherwise add_liquidity will + // fail with `AssetTwoDepositDidNotMeetMinimum`) + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 10000)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(user), 2, pool_account, 1)); + + // add_liquidity shouldn't fail because of the number of consumers + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 100, + 10000, + 10, + user, + )); + }); +} diff --git a/frame/asset-conversion/src/types.rs b/frame/asset-conversion/src/types.rs new file mode 100644 index 0000000000000..837b14be283ef --- /dev/null +++ b/frame/asset-conversion/src/types.rs @@ -0,0 +1,131 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use core::marker::PhantomData; +use sp_std::cmp::Ordering; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::fungibles::Inspect; +use scale_info::TypeInfo; + +pub(super) type AssetBalanceOf = + <::Assets as Inspect<::AccountId>>::Balance; +pub(super) type PoolIdOf = (::MultiAssetId, ::MultiAssetId); + +/// Stores the lp_token asset id a particular pool has been assigned. +#[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +pub struct PoolInfo { + /// Liquidity pool asset + pub lp_token: PoolAssetId, +} + +/// 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. + fn get_native() -> MultiAssetId; + + /// Returns true if the given MultiAssetId is the native currency. + fn is_native(asset: &MultiAssetId) -> bool; + + /// If it's not native, returns the AssetId for the given MultiAssetId. + fn try_convert(asset: &MultiAssetId) -> Result; + + /// Wrapps an AssetId as a MultiAssetId. + fn into_multiasset_id(asset: &AssetId) -> MultiAssetId; +} + +/// Benchmark Helper +#[cfg(feature = "runtime-benchmarks")] +pub trait BenchmarkHelper { + /// Returns an asset id from a given integer. + fn asset_id(asset_id: u32) -> AssetId; +} + +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkHelper for () +where + AssetId: From, +{ + fn asset_id(asset_id: u32) -> AssetId { + asset_id.into() + } +} + +/// An implementation of MultiAssetId that can be either Native or an asset. +#[derive(Decode, Encode, Default, MaxEncodedLen, TypeInfo, Clone, Copy, Debug)] +pub enum NativeOrAssetId +where + AssetId: Ord, +{ + /// Native asset. For example, on statemint this would be dot. + #[default] + Native, + /// A non-native asset id. + Asset(AssetId), +} + +impl Ord for NativeOrAssetId { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (Self::Native, Self::Native) => Ordering::Equal, + (Self::Native, Self::Asset(_)) => Ordering::Less, + (Self::Asset(_), Self::Native) => Ordering::Greater, + (Self::Asset(id1), Self::Asset(id2)) => ::cmp(id1, id2), + } + } +} +impl PartialOrd for NativeOrAssetId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(::cmp(self, other)) + } +} +impl PartialEq for NativeOrAssetId { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} +impl Eq for NativeOrAssetId {} + +/// Converts between a MultiAssetId and an AssetId +/// (or the native currency). +pub struct NativeOrAssetIdConverter { + _phantom: PhantomData, +} + +impl MultiAssetIdConverter, AssetId> + for NativeOrAssetIdConverter +{ + fn get_native() -> NativeOrAssetId { + NativeOrAssetId::Native + } + + fn is_native(asset: &NativeOrAssetId) -> bool { + *asset == Self::get_native() + } + + fn try_convert(asset: &NativeOrAssetId) -> Result { + match asset { + NativeOrAssetId::Asset(asset) => Ok(asset.clone()), + NativeOrAssetId::Native => Err(()), + } + } + + fn into_multiasset_id(asset: &AssetId) -> NativeOrAssetId { + NativeOrAssetId::Asset((*asset).clone()) + } +} diff --git a/frame/asset-conversion/src/weights.rs b/frame/asset-conversion/src/weights.rs new file mode 100644 index 0000000000000..9a526941c4553 --- /dev/null +++ b/frame/asset-conversion/src/weights.rs @@ -0,0 +1,245 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_dex +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-03-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/production/substrate +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/var/lib/gitlab-runner/builds/zyw4fam_/0/parity/mirrors/substrate/.git/.artifacts/bench.json +// --pallet=pallet_dex +// --chain=dev +// --header=./HEADER-APACHE2 +// --output=./frame/dex/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_dex. +pub trait WeightInfo { + fn create_pool() -> Weight; + fn add_liquidity() -> Weight; + fn remove_liquidity() -> Weight; + fn swap_exact_tokens_for_tokens() -> Weight; + fn swap_tokens_for_exact_tokens() -> Weight; +} + +/// Weights for pallet_dex using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Dex Pools (r:1 w:1) + /// Proof: Dex Pools (max_values: None, max_size: Some(30), added: 2505, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Dex NextPoolAssetId (r:1 w:1) + /// Proof: Dex NextPoolAssetId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: PoolAssets Asset (r:1 w:1) + /// Proof: PoolAssets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + fn create_pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `323` + // Estimated: `14855` + // Minimum execution time: 72_228_000 picoseconds. + Weight::from_parts(72_932_000, 14855) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: Dex Pools (r:1 w:0) + /// Proof: Dex Pools (max_values: None, max_size: Some(30), added: 2505, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:2 w:2) + /// Proof: Assets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + /// Storage: PoolAssets Asset (r:1 w:1) + /// Proof: PoolAssets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: PoolAssets Account (r:2 w:2) + /// Proof: PoolAssets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + fn add_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `1232` + // Estimated: `26726` + // Minimum execution time: 137_656_000 picoseconds. + Weight::from_parts(138_526_000, 26726) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: Dex Pools (r:1 w:0) + /// Proof: Dex Pools (max_values: None, max_size: Some(30), added: 2505, mode: MaxEncodedLen) + /// Storage: PoolAssets Asset (r:1 w:1) + /// Proof: PoolAssets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: PoolAssets Account (r:2 w:2) + /// Proof: PoolAssets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:2 w:2) + /// Proof: Assets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + fn remove_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `1496` + // Estimated: `26726` + // Minimum execution time: 159_778_000 picoseconds. + Weight::from_parts(160_730_000, 26726) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:2 w:2) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:4 w:4) + /// Proof: Assets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + fn swap_exact_tokens_for_tokens() -> Weight { + // Proof Size summary in bytes: + // Measured: `1134` + // Estimated: `21251` + // Minimum execution time: 136_852_000 picoseconds. + Weight::from_parts(137_764_000, 21251) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: Assets Asset (r:2 w:2) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:4 w:4) + /// Proof: Assets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn swap_tokens_for_exact_tokens() -> Weight { + // Proof Size summary in bytes: + // Measured: `1134` + // Estimated: `21251` + // Minimum execution time: 136_660_000 picoseconds. + Weight::from_parts(137_522_000, 21251) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Dex Pools (r:1 w:1) + /// Proof: Dex Pools (max_values: None, max_size: Some(30), added: 2505, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Dex NextPoolAssetId (r:1 w:1) + /// Proof: Dex NextPoolAssetId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: PoolAssets Asset (r:1 w:1) + /// Proof: PoolAssets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + fn create_pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `323` + // Estimated: `14855` + // Minimum execution time: 72_228_000 picoseconds. + Weight::from_parts(72_932_000, 14855) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: Dex Pools (r:1 w:0) + /// Proof: Dex Pools (max_values: None, max_size: Some(30), added: 2505, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:2 w:2) + /// Proof: Assets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + /// Storage: PoolAssets Asset (r:1 w:1) + /// Proof: PoolAssets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: PoolAssets Account (r:2 w:2) + /// Proof: PoolAssets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + fn add_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `1232` + // Estimated: `26726` + // Minimum execution time: 137_656_000 picoseconds. + Weight::from_parts(138_526_000, 26726) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } + /// Storage: Dex Pools (r:1 w:0) + /// Proof: Dex Pools (max_values: None, max_size: Some(30), added: 2505, mode: MaxEncodedLen) + /// Storage: PoolAssets Asset (r:1 w:1) + /// Proof: PoolAssets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: PoolAssets Account (r:2 w:2) + /// Proof: PoolAssets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:2 w:2) + /// Proof: Assets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + fn remove_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `1496` + // Estimated: `26726` + // Minimum execution time: 159_778_000 picoseconds. + Weight::from_parts(160_730_000, 26726) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:2 w:2) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:4 w:4) + /// Proof: Assets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + fn swap_exact_tokens_for_tokens() -> Weight { + // Proof Size summary in bytes: + // Measured: `1134` + // Estimated: `21251` + // Minimum execution time: 136_852_000 picoseconds. + Weight::from_parts(137_764_000, 21251) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } + /// Storage: Assets Asset (r:2 w:2) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:4 w:4) + /// Proof: Assets Account (max_values: None, max_size: Some(102), added: 2577, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn swap_tokens_for_exact_tokens() -> Weight { + // Proof Size summary in bytes: + // Measured: `1134` + // Estimated: `21251` + // Minimum execution time: 136_660_000 picoseconds. + Weight::from_parts(137_522_000, 21251) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } +} diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 8d0f8aa88dc5e..ebcf328d71474 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -90,8 +90,8 @@ pub use hooks::{ pub mod schedule; mod storage; pub use storage::{ - Instance, PartialStorageInfoTrait, StorageInfo, StorageInfoTrait, StorageInstance, - TrackedStorageKey, WhitelistedStorageKeys, + Incrementable, Instance, PartialStorageInfoTrait, StorageInfo, StorageInfoTrait, + StorageInstance, TrackedStorageKey, WhitelistedStorageKeys, }; mod dispatch; diff --git a/frame/support/src/traits/storage.rs b/frame/support/src/traits/storage.rs index c3394185a7743..dcd0843b88da9 100644 --- a/frame/support/src/traits/storage.rs +++ b/frame/support/src/traits/storage.rs @@ -20,6 +20,7 @@ use crate::sp_std::collections::btree_set::BTreeSet; use impl_trait_for_tuples::impl_for_tuples; pub use sp_core::storage::TrackedStorageKey; +use sp_runtime::traits::Saturating; use sp_std::prelude::*; /// An instance of a pallet in the storage. @@ -120,3 +121,29 @@ impl WhitelistedStorageKeys for Tuple { combined_keys.into_iter().collect::>() } } + +macro_rules! impl_incrementable { + ($($type:ty),+) => { + $( + impl Incrementable for $type { + fn increment(&self) -> Self { + let mut val = self.clone(); + val.saturating_inc(); + val + } + + fn initial_value() -> Self { + 0 + } + } + )+ + }; +} + +/// For example: allows new identifiers to be created in a linear fashion. +pub trait Incrementable { + fn increment(&self) -> Self; + fn initial_value() -> Self; +} + +impl_incrementable!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); diff --git a/frame/support/src/traits/tokens/fungibles/mod.rs b/frame/support/src/traits/tokens/fungibles/mod.rs index 697eff39ff748..10ca82b874c5b 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, Unbalanced, + Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, SwapForNative, Unbalanced, }; diff --git a/frame/support/src/traits/tokens/fungibles/regular.rs b/frame/support/src/traits/tokens/fungibles/regular.rs index 5a9d3e6e4b56e..47474eb1128ea 100644 --- a/frame/support/src/traits/tokens/fungibles/regular.rs +++ b/frame/support/src/traits/tokens/fungibles/regular.rs @@ -583,3 +583,16 @@ 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) {} } + +/// Use an on-chain exchange to convert the asset to the equivalent in native tokens. +pub trait SwapForNative { + // If successful returns the amount in native tokens. + 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; +}