mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-27 11:38:01 +00:00
4f832ea865
closes https://github.com/paritytech/polkadot-sdk/issues/1842 Decoupling Pallet from the Concept of Native Currency Currently, the pallet is intrinsically linked with the concept of native currency, requiring users to provide implementations of the `fungible::*` and `fungibles::*` traits to interact with native and non native assets. This incapsulates some non-related to the pallet complexity and makes it less adaptable in contexts where the native currency concept is absent. With this PR, the dependence on `fungible::*` for liquidity-supplying assets has been removed. Instead, the native and non-native currencies' handling is now overseen by a single type that implements the `fungibles::*` traits. To simplify this integration, types have been introduced to facilitate the creation of a union between `fungible::*` and `fungibles::*` implementations, producing a unified `fungibles::*` type. One of the reasons driving these changes is the ambition to create a more user-friendly API for the `SwapCredit` implementation. Given that it interacts with two distinct credit types from `fungible` and `fungibles`, a unified type was introduced. Clients now manage potential conversion failures for those credit types. In certain contexts, it's vital to guarantee that operations are fail-safe, like in this impl - [PR](https://github.com/paritytech/polkadot-sdk/pull/1845), place in [code](https://github.com/paritytech/polkadot-sdk/blob/20b85a5fada8f55c98ba831964f5866ffeadf4da/cumulus/primitives/utility/src/lib.rs#L429). Additional Updates: - abstracted the pool ID and its account derivation logic via trait bounds, along with common implementation offerings; - removed `inc_providers` on a pool creation for the pool account; - benchmarks: -- swap complexity is N, not const; -- removed `From<u128> + Into<u128>` bound from `T::Balance`; -- removed swap/liquidity/.. amount constants, resolve them dynamically based on pallet configuration; -- migrated to v2 API; - `OnUnbalanced` handler for the pool creation fee, replacing direct transfers to a specified account ID; - renamed `MultiAssetId` to `AssetKind` aligning with naming across frame crates; related PRs: - (depends) https://github.com/paritytech/polkadot-sdk/pull/1677 - (caused) https://github.com/paritytech/polkadot-sdk/pull/2033 - (caused) https://github.com/paritytech/polkadot-sdk/pull/1876 --------- Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>
2496 lines
67 KiB
Rust
2496 lines
67 KiB
Rust
// 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.
|
|
|
|
use crate::{mock::*, *};
|
|
use frame_support::{
|
|
assert_noop, assert_ok, assert_storage_noop,
|
|
instances::Instance1,
|
|
traits::{
|
|
fungible,
|
|
fungible::{Inspect as FungibleInspect, NativeOrWithId},
|
|
fungibles,
|
|
fungibles::{Inspect, InspectEnumerable},
|
|
Get,
|
|
},
|
|
};
|
|
use sp_arithmetic::Permill;
|
|
use sp_runtime::{DispatchError, TokenError};
|
|
|
|
fn events() -> Vec<Event<Test>> {
|
|
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<<Test as Config>::PoolId> {
|
|
let mut s: Vec<_> = Pools::<Test>::iter().map(|x| x.0).collect();
|
|
s.sort();
|
|
s
|
|
}
|
|
|
|
fn assets() -> Vec<NativeOrWithId<u32>> {
|
|
let mut s: Vec<_> = Assets::asset_ids().map(|id| NativeOrWithId::WithId(id)).collect();
|
|
s.sort();
|
|
s
|
|
}
|
|
|
|
fn pool_assets() -> Vec<u32> {
|
|
let mut s: Vec<_> = <<Test as Config>::PoolAssets>::asset_ids().collect();
|
|
s.sort();
|
|
s
|
|
}
|
|
|
|
fn create_tokens(owner: u128, tokens: Vec<NativeOrWithId<u32>>) {
|
|
create_tokens_with_ed(owner, tokens, 1)
|
|
}
|
|
|
|
fn create_tokens_with_ed(owner: u128, tokens: Vec<NativeOrWithId<u32>>, ed: u128) {
|
|
for token_id in tokens {
|
|
let asset_id = match token_id {
|
|
NativeOrWithId::WithId(id) => id,
|
|
_ => unreachable!("invalid token"),
|
|
};
|
|
assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, false, ed));
|
|
}
|
|
}
|
|
|
|
fn balance(owner: u128, token_id: NativeOrWithId<u32>) -> u128 {
|
|
<<Test as Config>::Assets>::balance(token_id, &owner)
|
|
}
|
|
|
|
fn pool_balance(owner: u128, token_id: u32) -> u128 {
|
|
<<Test as Config>::PoolAssets>::balance(token_id, owner)
|
|
}
|
|
|
|
fn get_native_ed() -> u128 {
|
|
<<Test as Config>::Assets>::minimum_balance(NativeOrWithId::Native)
|
|
}
|
|
|
|
macro_rules! bvec {
|
|
($($x:expr),+ $(,)?) => (
|
|
vec![$( Box::new( $x ), )*]
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn validate_with_first_asset_pool_id_locator() {
|
|
new_test_ext().execute_with(|| {
|
|
use NativeOrWithId::{Native, WithId};
|
|
assert_eq!(WithFirstAssetLocator::pool_id(&Native, &WithId(2)), Ok((Native, WithId(2))));
|
|
assert_eq!(WithFirstAssetLocator::pool_id(&WithId(2), &Native), Ok((Native, WithId(2))));
|
|
assert_noop!(WithFirstAssetLocator::pool_id(&Native, &Native), ());
|
|
assert_noop!(WithFirstAssetLocator::pool_id(&WithId(2), &WithId(1)), ());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn validate_ascending_pool_id_locator() {
|
|
new_test_ext().execute_with(|| {
|
|
use NativeOrWithId::{Native, WithId};
|
|
assert_eq!(AscendingLocator::pool_id(&Native, &WithId(2)), Ok((Native, WithId(2))));
|
|
assert_eq!(AscendingLocator::pool_id(&WithId(2), &Native), Ok((Native, WithId(2))));
|
|
assert_eq!(AscendingLocator::pool_id(&WithId(2), &WithId(1)), Ok((WithId(1), WithId(2))));
|
|
assert_eq!(AscendingLocator::pool_id(&Native, &Native), Err(()));
|
|
assert_eq!(AscendingLocator::pool_id(&WithId(1), &WithId(1)), Err(()));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn validate_native_or_with_id_sorting() {
|
|
new_test_ext().execute_with(|| {
|
|
use NativeOrWithId::{Native, WithId};
|
|
assert!(WithId(2) > WithId(1));
|
|
assert!(WithId(1) <= WithId(1));
|
|
assert_eq!(WithId(1), WithId(1));
|
|
assert_eq!(Native::<u32>, Native::<u32>);
|
|
assert!(Native < WithId(1));
|
|
});
|
|
}
|
|
|
|
#[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: u128 = <Test as Config>::PoolLocator::address(&(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(i),
|
|
))
|
|
.unwrap();
|
|
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 =
|
|
<mock::Test as pallet_assets::Config<Instance1>>::AssetAccountDeposit::get();
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let pool_id = (token_1.clone(), token_2.clone());
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
|
|
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),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_1.clone())
|
|
));
|
|
|
|
let setup_fee = <<Test as Config>::PoolSetupFee as Get<<Test as Config>::Balance>>::get();
|
|
let pool_account = AssetConversionOrigin::get();
|
|
assert_eq!(
|
|
balance(user, NativeOrWithId::Native),
|
|
1000 - (setup_fee + asset_account_deposit)
|
|
);
|
|
assert_eq!(balance(pool_account, NativeOrWithId::Native), setup_fee);
|
|
assert_eq!(lp_token + 1, AssetConversion::get_next_pool_asset_id());
|
|
|
|
assert_eq!(
|
|
events(),
|
|
[Event::<Test>::PoolCreated {
|
|
creator: user,
|
|
pool_id: pool_id.clone(),
|
|
pool_account: <Test as Config>::PoolLocator::address(&pool_id).unwrap(),
|
|
lp_token
|
|
}]
|
|
);
|
|
assert_eq!(pools(), vec![pool_id]);
|
|
assert_eq!(assets(), vec![token_2.clone()]);
|
|
assert_eq!(pool_assets(), vec![lp_token]);
|
|
|
|
assert_noop!(
|
|
AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_1.clone())
|
|
),
|
|
Error::<Test>::InvalidAssetPair
|
|
);
|
|
assert_noop!(
|
|
AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_2.clone())
|
|
),
|
|
Error::<Test>::InvalidAssetPair
|
|
);
|
|
|
|
// validate we cannot create WithId(1)/WithId(2) pool
|
|
let token_1 = NativeOrWithId::WithId(1);
|
|
create_tokens(user, vec![token_1.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn create_same_pool_twice_should_fail() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
|
|
let lp_token = AssetConversion::get_next_pool_asset_id();
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_1.clone())
|
|
));
|
|
let expected_free = lp_token + 1;
|
|
assert_eq!(expected_free, AssetConversion::get_next_pool_asset_id());
|
|
|
|
assert_noop!(
|
|
AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_1.clone())
|
|
),
|
|
Error::<Test>::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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
),
|
|
Error::<Test>::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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let token_3 = NativeOrWithId::WithId(3);
|
|
let pool_id_1_2 = (token_1.clone(), token_2.clone());
|
|
let pool_id_1_3 = (token_1.clone(), token_3.clone());
|
|
|
|
create_tokens(user, vec![token_2.clone(), token_3.clone()]);
|
|
|
|
let lp_token2_1 = AssetConversion::get_next_pool_asset_id();
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_1.clone())
|
|
));
|
|
let lp_token3_1 = AssetConversion::get_next_pool_asset_id();
|
|
|
|
assert_eq!(
|
|
events(),
|
|
[Event::<Test>::PoolCreated {
|
|
creator: user,
|
|
pool_id: pool_id_1_2.clone(),
|
|
pool_account: <Test as Config>::PoolLocator::address(&pool_id_1_2).unwrap(),
|
|
lp_token: lp_token2_1
|
|
}]
|
|
);
|
|
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_3.clone()),
|
|
Box::new(token_1.clone())
|
|
));
|
|
assert_eq!(
|
|
events(),
|
|
[Event::<Test>::PoolCreated {
|
|
creator: user,
|
|
pool_id: pool_id_1_3.clone(),
|
|
pool_account: <Test as Config>::PoolLocator::address(&pool_id_1_3).unwrap(),
|
|
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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let token_3 = NativeOrWithId::WithId(3);
|
|
|
|
create_tokens(user, vec![token_2.clone(), token_3.clone()]);
|
|
let lp_token1 = AssetConversion::get_next_pool_asset_id();
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
let lp_token2 = AssetConversion::get_next_pool_asset_id();
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_3.clone())
|
|
));
|
|
|
|
let ed = get_native_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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
10000,
|
|
10,
|
|
10000,
|
|
10,
|
|
user,
|
|
));
|
|
|
|
let pool_id = (token_1.clone(), token_2.clone());
|
|
assert!(events().contains(&Event::<Test>::LiquidityAdded {
|
|
who: user,
|
|
mint_to: user,
|
|
pool_id: pool_id.clone(),
|
|
amount1_provided: 10000,
|
|
amount2_provided: 10,
|
|
lp_token: lp_token1,
|
|
lp_token_minted: 216,
|
|
}));
|
|
let pallet_account = <Test as Config>::PoolLocator::address(&pool_id).unwrap();
|
|
assert_eq!(balance(pallet_account, token_1.clone()), 10000);
|
|
assert_eq!(balance(pallet_account, token_2.clone()), 10);
|
|
assert_eq!(balance(user, token_1.clone()), 10000 + ed);
|
|
assert_eq!(balance(user, token_2.clone()), 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),
|
|
Box::new(token_3.clone()),
|
|
Box::new(token_1.clone()),
|
|
10,
|
|
10000,
|
|
10,
|
|
10000,
|
|
user,
|
|
));
|
|
|
|
let pool_id = (token_1.clone(), token_3.clone());
|
|
assert!(events().contains(&Event::<Test>::LiquidityAdded {
|
|
who: user,
|
|
mint_to: user,
|
|
pool_id: pool_id.clone(),
|
|
amount1_provided: 10,
|
|
amount2_provided: 10000,
|
|
lp_token: lp_token2,
|
|
lp_token_minted: 216,
|
|
}));
|
|
let pallet_account = <Test as Config>::PoolLocator::address(&pool_id).unwrap();
|
|
assert_eq!(balance(pallet_account, token_1.clone()), 10000);
|
|
assert_eq!(balance(pallet_account, token_3.clone()), 10);
|
|
assert_eq!(balance(user, token_1.clone()), ed);
|
|
assert_eq!(balance(user, token_3.clone()), 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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
user
|
|
),
|
|
Error::<Test>::AmountOneLessThanMinimal
|
|
);
|
|
|
|
assert_noop!(
|
|
AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
get_native_ed(),
|
|
1,
|
|
1,
|
|
1,
|
|
user
|
|
),
|
|
Error::<Test>::InsufficientLiquidityMinted
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn add_tiny_liquidity_directly_to_pool_address() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let token_3 = NativeOrWithId::WithId(3);
|
|
|
|
create_tokens(user, vec![token_2.clone(), token_3.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_3.clone())
|
|
));
|
|
|
|
let ed = get_native_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.clone()
|
|
let pallet_account =
|
|
<Test as Config>::PoolLocator::address(&(token_1.clone(), token_2.clone())).unwrap();
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), pallet_account, 1000));
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
10000,
|
|
10,
|
|
10000,
|
|
10,
|
|
user,
|
|
));
|
|
|
|
// check the same but for token_3.clone() (non-native token)
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_3.clone()),
|
|
10000,
|
|
10,
|
|
10000,
|
|
10,
|
|
user,
|
|
));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn can_remove_liquidity() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let pool_id = (token_1.clone(), token_2.clone());
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
let lp_token = AssetConversion::get_next_pool_asset_id();
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed_token_1 = <Balances as fungible::Inspect<_>>::minimum_balance();
|
|
let ed_token_2 = <Assets as fungibles::Inspect<_>>::minimum_balance(2);
|
|
assert_ok!(Balances::force_set_balance(
|
|
RuntimeOrigin::root(),
|
|
user,
|
|
10000000000 + ed_token_1
|
|
));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 100000 + ed_token_2));
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
total_lp_received,
|
|
0,
|
|
0,
|
|
user,
|
|
));
|
|
|
|
assert!(events().contains(&Event::<Test>::LiquidityRemoved {
|
|
who: user,
|
|
withdraw_to: user,
|
|
pool_id: pool_id.clone(),
|
|
amount1: 899991000,
|
|
amount2: 89999,
|
|
lp_token,
|
|
lp_token_burned: total_lp_received,
|
|
withdrawal_fee: <Test as Config>::LiquidityWithdrawalFee::get()
|
|
}));
|
|
|
|
let pool_account = <Test as Config>::PoolLocator::address(&pool_id).unwrap();
|
|
assert_eq!(balance(pool_account, token_1.clone()), 100009000);
|
|
assert_eq!(balance(pool_account, token_2.clone()), 10001);
|
|
assert_eq!(pool_balance(pool_account, lp_token), 100);
|
|
|
|
assert_eq!(
|
|
balance(user, token_1.clone()),
|
|
10000000000 - 1000000000 + 899991000 + ed_token_1
|
|
);
|
|
assert_eq!(balance(user, token_2.clone()), 89999 + ed_token_2);
|
|
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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let lp_token = AssetConversion::get_next_pool_asset_id();
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
assert_ok!(Balances::force_set_balance(
|
|
RuntimeOrigin::root(),
|
|
user,
|
|
10000 + get_native_ed()
|
|
));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000));
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
10000,
|
|
200,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
assert_eq!(
|
|
AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
3000,
|
|
false,
|
|
),
|
|
Some(60)
|
|
);
|
|
// including fee so should get less out...
|
|
assert_eq!(
|
|
AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
3000,
|
|
true,
|
|
),
|
|
Some(46)
|
|
);
|
|
// 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(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
3000,
|
|
false,
|
|
),
|
|
Some(60)
|
|
);
|
|
// including fee so should get less out...
|
|
assert_eq!(
|
|
AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
3000,
|
|
true,
|
|
),
|
|
Some(46)
|
|
);
|
|
|
|
// Check inverse:
|
|
assert_eq!(
|
|
AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::WithId(2),
|
|
NativeOrWithId::Native,
|
|
60,
|
|
false,
|
|
),
|
|
Some(3000)
|
|
);
|
|
// including fee so should get less out...
|
|
assert_eq!(
|
|
AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::WithId(2),
|
|
NativeOrWithId::Native,
|
|
60,
|
|
true,
|
|
),
|
|
Some(2302)
|
|
);
|
|
|
|
//
|
|
// same tests as above but for quote_price_tokens_for_exact_tokens:
|
|
//
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
60,
|
|
false,
|
|
),
|
|
Some(3000)
|
|
);
|
|
// including fee so should need to put more in...
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
60,
|
|
true,
|
|
),
|
|
Some(4299)
|
|
);
|
|
// Check it still gives same price:
|
|
// (if the above accidentally exchanged then it would not give same quote as before)
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
60,
|
|
false,
|
|
),
|
|
Some(3000)
|
|
);
|
|
// including fee so should need to put more in...
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
60,
|
|
true,
|
|
),
|
|
Some(4299)
|
|
);
|
|
|
|
// Check inverse:
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::WithId(2),
|
|
NativeOrWithId::Native,
|
|
3000,
|
|
false,
|
|
),
|
|
Some(60)
|
|
);
|
|
// including fee so should need to put more in...
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::WithId(2),
|
|
NativeOrWithId::Native,
|
|
3000,
|
|
true,
|
|
),
|
|
Some(86)
|
|
);
|
|
|
|
//
|
|
// roundtrip: Without fees one should get the original number
|
|
//
|
|
let amount_in = 100;
|
|
|
|
assert_eq!(
|
|
AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::WithId(2),
|
|
NativeOrWithId::Native,
|
|
amount_in,
|
|
false,
|
|
)
|
|
.and_then(|amount| AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
amount,
|
|
false,
|
|
)),
|
|
Some(amount_in)
|
|
);
|
|
assert_eq!(
|
|
AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
amount_in,
|
|
false,
|
|
)
|
|
.and_then(|amount| AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
NativeOrWithId::WithId(2),
|
|
NativeOrWithId::Native,
|
|
amount,
|
|
false,
|
|
)),
|
|
Some(amount_in)
|
|
);
|
|
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::WithId(2),
|
|
NativeOrWithId::Native,
|
|
amount_in,
|
|
false,
|
|
)
|
|
.and_then(|amount| AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
amount,
|
|
false,
|
|
)),
|
|
Some(amount_in)
|
|
);
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::Native,
|
|
NativeOrWithId::WithId(2),
|
|
amount_in,
|
|
false,
|
|
)
|
|
.and_then(|amount| AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
NativeOrWithId::WithId(2),
|
|
NativeOrWithId::Native,
|
|
amount,
|
|
false,
|
|
)),
|
|
Some(amount_in)
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn quote_price_exact_tokens_for_tokens_matches_execution() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let user2 = 2;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
10000,
|
|
200,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
let amount = 1;
|
|
let quoted_price = 49;
|
|
assert_eq!(
|
|
AssetConversion::quote_price_exact_tokens_for_tokens(
|
|
token_2.clone(),
|
|
token_1.clone(),
|
|
amount,
|
|
true,
|
|
),
|
|
Some(quoted_price)
|
|
);
|
|
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, amount));
|
|
let prior_dot_balance = 20000;
|
|
assert_eq!(prior_dot_balance, balance(user2, token_1.clone()));
|
|
assert_ok!(AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user2),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
amount,
|
|
1,
|
|
user2,
|
|
false,
|
|
));
|
|
|
|
assert_eq!(prior_dot_balance + quoted_price, balance(user2, token_1.clone()));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn quote_price_tokens_for_exact_tokens_matches_execution() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let user2 = 2;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
10000,
|
|
200,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
let amount = 49;
|
|
let quoted_price = 1;
|
|
assert_eq!(
|
|
AssetConversion::quote_price_tokens_for_exact_tokens(
|
|
token_2.clone(),
|
|
token_1.clone(),
|
|
amount,
|
|
true,
|
|
),
|
|
Some(quoted_price)
|
|
);
|
|
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, amount));
|
|
let prior_dot_balance = 20000;
|
|
assert_eq!(prior_dot_balance, balance(user2, token_1.clone()));
|
|
let prior_asset_balance = 49;
|
|
assert_eq!(prior_asset_balance, balance(user2, token_2.clone()));
|
|
assert_ok!(AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user2),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
amount,
|
|
1,
|
|
user2,
|
|
false,
|
|
));
|
|
|
|
assert_eq!(prior_dot_balance + amount, balance(user2, token_1.clone()));
|
|
assert_eq!(prior_asset_balance - quoted_price, balance(user2, token_2.clone()));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn can_swap_with_native() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let pool_id = (token_1.clone(), token_2.clone());
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
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.clone(), token_1.clone()],
|
|
input_amount,
|
|
1,
|
|
user,
|
|
false,
|
|
));
|
|
|
|
let pallet_account = <Test as Config>::PoolLocator::address(&pool_id).unwrap();
|
|
assert_eq!(balance(user, token_1.clone()), expect_receive + ed);
|
|
assert_eq!(balance(user, token_2.clone()), 1000 - liquidity2 - input_amount);
|
|
assert_eq!(balance(pallet_account, token_1.clone()), liquidity1 - expect_receive);
|
|
assert_eq!(balance(pallet_account, token_2.clone()), liquidity2 + input_amount);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn can_swap_with_realistic_values() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let dot = NativeOrWithId::Native;
|
|
let usd = NativeOrWithId::WithId(2);
|
|
create_tokens(user, vec![usd.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(dot.clone()),
|
|
Box::new(usd.clone())
|
|
));
|
|
|
|
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),
|
|
Box::new(dot.clone()),
|
|
Box::new(usd.clone()),
|
|
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.clone(), dot.clone()],
|
|
input_amount,
|
|
1,
|
|
user,
|
|
false,
|
|
));
|
|
|
|
assert!(events().contains(&Event::<Test>::SwapExecuted {
|
|
who: user,
|
|
send_to: user,
|
|
amount_in: 10 * UNIT, // usd
|
|
amount_out: 1_993_980_120, // About 2 dot after div by UNIT.
|
|
path: vec![(usd, 10 * UNIT), (dot, 1_993_980_120)],
|
|
}));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn can_not_swap_in_pool_with_no_liquidity_added_yet() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
// Check can't swap an empty pool
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
10,
|
|
1,
|
|
user,
|
|
false,
|
|
),
|
|
Error::<Test>::PoolNotFound
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn check_no_panic_when_try_swap_close_to_empty_pool() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let pool_id = (token_1.clone(), token_2.clone());
|
|
let lp_token = AssetConversion::get_next_pool_asset_id();
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
let lp_token_minted = pool_balance(user, lp_token);
|
|
assert!(events().contains(&Event::<Test>::LiquidityAdded {
|
|
who: user,
|
|
mint_to: user,
|
|
pool_id: pool_id.clone(),
|
|
amount1_provided: liquidity1,
|
|
amount2_provided: liquidity2,
|
|
lp_token,
|
|
lp_token_minted,
|
|
}));
|
|
|
|
let pallet_account = <Test as Config>::PoolLocator::address(&pool_id).unwrap();
|
|
assert_eq!(balance(pallet_account, token_1.clone()), liquidity1);
|
|
assert_eq!(balance(pallet_account, token_2.clone()), liquidity2);
|
|
|
|
assert_ok!(AssetConversion::remove_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
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.clone()), 708);
|
|
assert_eq!(balance(pallet_account, token_2.clone()), 15);
|
|
|
|
// validate the reserve should always stay above the ED
|
|
assert_noop!(
|
|
AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
708 - ed + 1, // amount_out
|
|
500, // amount_in_max
|
|
user,
|
|
false,
|
|
),
|
|
TokenError::NotExpendable,
|
|
);
|
|
|
|
assert_ok!(AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
608, // amount_out
|
|
500, // amount_in_max
|
|
user,
|
|
false,
|
|
));
|
|
|
|
let token_1_left = balance(pallet_account, token_1.clone());
|
|
let token_2_left = balance(pallet_account, token_2.clone());
|
|
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.clone(), token_1.clone()],
|
|
token_1_left - 1, // amount_out
|
|
1000, // amount_in_max
|
|
user,
|
|
false,
|
|
),
|
|
Error::<Test>::ProvidedMaximumNotSufficientForSwap
|
|
);
|
|
|
|
// Try to swap what's left in the pool
|
|
assert_noop!(
|
|
AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
token_1_left, // amount_out
|
|
1000, // amount_in_max
|
|
user,
|
|
false,
|
|
),
|
|
Error::<Test>::AmountOutTooHigh
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn swap_should_not_work_if_too_much_slippage() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
assert_ok!(Balances::force_set_balance(
|
|
RuntimeOrigin::root(),
|
|
user,
|
|
10000 + get_native_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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
let exchange_amount = 100;
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
exchange_amount, // amount_in
|
|
4000, // amount_out_min
|
|
user,
|
|
false,
|
|
),
|
|
Error::<Test>::ProvidedMinimumNotSufficientForSwap
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn can_swap_tokens_for_exact_tokens() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let pool_id = (token_1.clone(), token_2.clone());
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_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 = <Test as Config>::PoolLocator::address(&pool_id).unwrap();
|
|
let before1 = balance(pallet_account, token_1.clone()) + balance(user, token_1.clone());
|
|
let before2 = balance(pallet_account, token_2.clone()) + balance(user, token_2.clone());
|
|
|
|
let liquidity1 = 10000;
|
|
let liquidity2 = 200;
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
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.clone(), token_2.clone()],
|
|
exchange_out, // amount_out
|
|
3500, // amount_in_max
|
|
user,
|
|
true,
|
|
));
|
|
|
|
assert_eq!(balance(user, token_1.clone()), 10000 + ed - expect_in);
|
|
assert_eq!(balance(user, token_2.clone()), 1000 - liquidity2 + exchange_out);
|
|
assert_eq!(balance(pallet_account, token_1.clone()), liquidity1 + expect_in);
|
|
assert_eq!(balance(pallet_account, token_2.clone()), liquidity2 - exchange_out);
|
|
|
|
// check invariants:
|
|
|
|
// native and asset totals should be preserved.
|
|
assert_eq!(
|
|
before1,
|
|
balance(pallet_account, token_1.clone()) + balance(user, token_1.clone())
|
|
);
|
|
assert_eq!(
|
|
before2,
|
|
balance(pallet_account, token_2.clone()) + balance(user, token_2.clone())
|
|
);
|
|
});
|
|
}
|
|
|
|
#[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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let pool_id = (token_1.clone(), token_2.clone());
|
|
let lp_token = AssetConversion::get_next_pool_asset_id();
|
|
|
|
create_tokens(user2, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_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 = <Test as Config>::PoolLocator::address(&pool_id).unwrap();
|
|
let before1 = balance(pallet_account, token_1.clone()) +
|
|
balance(user, token_1.clone()) +
|
|
balance(user2, token_1.clone());
|
|
let before2 = balance(pallet_account, token_2.clone()) +
|
|
balance(user, token_2.clone()) +
|
|
balance(user2, token_2.clone());
|
|
|
|
let liquidity1 = 10000;
|
|
let liquidity2 = 200;
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user2,
|
|
));
|
|
|
|
assert_eq!(balance(user, token_1.clone()), base1 + ed);
|
|
assert_eq!(balance(user, token_2.clone()), 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.clone(), token_2.clone()],
|
|
exchange_out, // amount_out
|
|
3500, // amount_in_max
|
|
user,
|
|
true,
|
|
));
|
|
|
|
assert_eq!(balance(user, token_1.clone()), base1 + ed - expect_in);
|
|
assert_eq!(balance(pallet_account, token_1.clone()), liquidity1 + expect_in);
|
|
assert_eq!(balance(user, token_2.clone()), exchange_out);
|
|
assert_eq!(balance(pallet_account, token_2.clone()), liquidity2 - exchange_out);
|
|
|
|
// check invariants:
|
|
|
|
// native and asset totals should be preserved.
|
|
assert_eq!(
|
|
before1,
|
|
balance(pallet_account, token_1.clone()) +
|
|
balance(user, token_1.clone()) +
|
|
balance(user2, token_1.clone())
|
|
);
|
|
assert_eq!(
|
|
before2,
|
|
balance(pallet_account, token_2.clone()) +
|
|
balance(user, token_2.clone()) +
|
|
balance(user2, token_2.clone())
|
|
);
|
|
|
|
let lp_token_minted = pool_balance(user2, lp_token);
|
|
assert_eq!(lp_token_minted, 1314);
|
|
|
|
assert_ok!(AssetConversion::remove_liquidity(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user2, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_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!(Assets::mint(RuntimeOrigin::signed(user2), 2, user, 2));
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
10000,
|
|
200,
|
|
1,
|
|
1,
|
|
user2,
|
|
));
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_1.clone(), token_2.clone()],
|
|
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.clone(), token_2.clone()],
|
|
51, // amount_in
|
|
1, // amount_out_min
|
|
user,
|
|
true,
|
|
),
|
|
DispatchError::Token(TokenError::NotExpendable)
|
|
);
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
51, // amount_out
|
|
2, // amount_in_max
|
|
user,
|
|
true,
|
|
),
|
|
DispatchError::Token(TokenError::NotExpendable)
|
|
);
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_2.clone(), token_1.clone()],
|
|
2, // amount_in
|
|
1, // amount_out_min
|
|
user,
|
|
true,
|
|
),
|
|
DispatchError::Token(TokenError::NotExpendable)
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn swap_when_existential_deposit_would_cause_reaping_pool_account() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let user2 = 2;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let token_3 = NativeOrWithId::WithId(3);
|
|
|
|
let ed_assets = 100;
|
|
create_tokens_with_ed(user2, vec![token_2.clone(), token_3.clone()], ed_assets);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_3.clone())
|
|
));
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_3.clone())
|
|
));
|
|
|
|
let ed = get_native_ed();
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed));
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user2, 400 + ed_assets));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 3, user2, 20000 + ed_assets));
|
|
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user, 400 + ed_assets));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 3, user, 20000 + ed_assets));
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
10000,
|
|
200,
|
|
1,
|
|
1,
|
|
user2,
|
|
));
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_3.clone()),
|
|
200,
|
|
10000,
|
|
1,
|
|
1,
|
|
user2,
|
|
));
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user2),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_3.clone()),
|
|
200,
|
|
10000,
|
|
1,
|
|
1,
|
|
user2,
|
|
));
|
|
|
|
// causes an account removal for asset token 2
|
|
assert_noop!(
|
|
AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_1.clone(), token_2.clone()],
|
|
110, // amount_out
|
|
20000, // amount_in_max
|
|
user,
|
|
true,
|
|
),
|
|
DispatchError::Token(TokenError::NotExpendable)
|
|
);
|
|
|
|
// causes an account removal for asset token 2
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_1.clone(), token_2.clone()],
|
|
15000, // amount_in
|
|
110, // amount_out_min
|
|
user,
|
|
true,
|
|
),
|
|
DispatchError::Token(TokenError::NotExpendable)
|
|
);
|
|
|
|
// causes an account removal for native token 1
|
|
assert_noop!(
|
|
AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_3.clone(), token_1.clone()],
|
|
110, // amount_out
|
|
20000, // amount_in_max
|
|
user,
|
|
true,
|
|
),
|
|
DispatchError::Token(TokenError::NotExpendable)
|
|
);
|
|
|
|
// causes an account removal for native token 1
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_3.clone(), token_1.clone()],
|
|
15000, // amount_in
|
|
110, // amount_out_min
|
|
user,
|
|
true,
|
|
),
|
|
DispatchError::Token(TokenError::NotExpendable)
|
|
);
|
|
|
|
// causes an account removal for native token 1 locate in the middle of a swap path
|
|
let amount_in = AssetConversion::balance_path_from_amount_out(
|
|
110,
|
|
vec![token_3.clone(), token_1.clone()],
|
|
)
|
|
.unwrap()
|
|
.first()
|
|
.map(|(_, a)| *a)
|
|
.unwrap();
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_3.clone(), token_1.clone(), token_2.clone()],
|
|
amount_in, // amount_in
|
|
1, // amount_out_min
|
|
user,
|
|
true,
|
|
),
|
|
DispatchError::Token(TokenError::NotExpendable)
|
|
);
|
|
|
|
// causes an account removal for asset token 2 locate in the middle of a swap path
|
|
let amount_in = AssetConversion::balance_path_from_amount_out(
|
|
110,
|
|
vec![token_1.clone(), token_2.clone()],
|
|
)
|
|
.unwrap()
|
|
.first()
|
|
.map(|(_, a)| *a)
|
|
.unwrap();
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_1.clone(), token_2.clone(), token_3.clone()],
|
|
amount_in, // 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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
assert_ok!(Balances::force_set_balance(
|
|
RuntimeOrigin::root(),
|
|
user,
|
|
20000 + get_native_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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
let exchange_out = 1;
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_tokens_for_exact_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_1.clone(), token_2.clone()],
|
|
exchange_out, // amount_out
|
|
50, // amount_in_max just greater than slippage.
|
|
user,
|
|
true
|
|
),
|
|
Error::<Test>::ProvidedMaximumNotSufficientForSwap
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn swap_exact_tokens_for_tokens_in_multi_hops() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let token_3 = NativeOrWithId::WithId(3);
|
|
|
|
create_tokens(user, vec![token_2.clone(), token_3.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_3.clone())
|
|
));
|
|
|
|
let ed = get_native_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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_3.clone()),
|
|
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.clone()],
|
|
input_amount,
|
|
80,
|
|
user,
|
|
true,
|
|
),
|
|
Error::<Test>::InvalidPath
|
|
);
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_1.clone(), token_2.clone(), token_3.clone(), token_2.clone()],
|
|
input_amount,
|
|
80,
|
|
user,
|
|
true,
|
|
),
|
|
Error::<Test>::NonUniquePath
|
|
);
|
|
|
|
assert_ok!(AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_1.clone(), token_2.clone(), token_3.clone()],
|
|
input_amount, // amount_in
|
|
80, // amount_out_min
|
|
user,
|
|
true,
|
|
));
|
|
|
|
let pool_id1 = (token_1.clone(), token_2.clone());
|
|
let pool_id2 = (token_2.clone(), token_3.clone());
|
|
let pallet_account1 = <Test as Config>::PoolLocator::address(&pool_id1).unwrap();
|
|
let pallet_account2 = <Test as Config>::PoolLocator::address(&pool_id2).unwrap();
|
|
|
|
assert_eq!(balance(user, token_1.clone()), base1 + ed - input_amount);
|
|
assert_eq!(balance(pallet_account1, token_1.clone()), liquidity1 + input_amount);
|
|
assert_eq!(balance(pallet_account1, token_2.clone()), liquidity2 - expect_out2);
|
|
assert_eq!(balance(pallet_account2, token_2.clone()), liquidity2 + expect_out2);
|
|
assert_eq!(balance(pallet_account2, token_3.clone()), liquidity3 - expect_out3);
|
|
assert_eq!(balance(user, token_3.clone()), 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 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let token_3 = NativeOrWithId::WithId(3);
|
|
|
|
create_tokens(user, vec![token_2.clone(), token_3.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_3.clone())
|
|
));
|
|
|
|
let ed = get_native_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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_2.clone()),
|
|
Box::new(token_3.clone()),
|
|
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.clone(), token_2.clone(), token_3.clone()],
|
|
exchange_out3, // amount_out
|
|
1000, // amount_in_max
|
|
user,
|
|
true,
|
|
));
|
|
|
|
let pool_id1 = (token_1.clone(), token_2.clone());
|
|
let pool_id2 = (token_2.clone(), token_3.clone());
|
|
let pallet_account1 = <Test as Config>::PoolLocator::address(&pool_id1).unwrap();
|
|
let pallet_account2 = <Test as Config>::PoolLocator::address(&pool_id2).unwrap();
|
|
|
|
assert_eq!(balance(user, token_1.clone()), base1 + ed - expect_in1);
|
|
assert_eq!(balance(pallet_account1, token_1.clone()), liquidity1 + expect_in1);
|
|
assert_eq!(balance(pallet_account1, token_2.clone()), liquidity2 - expect_in2);
|
|
assert_eq!(balance(pallet_account2, token_2.clone()), liquidity2 + expect_in2);
|
|
assert_eq!(balance(pallet_account2, token_3.clone()), liquidity3 - exchange_out3);
|
|
assert_eq!(balance(user, token_3.clone()), 10000 - liquidity3 + exchange_out3);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn can_not_swap_same_asset() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::WithId(1);
|
|
let token_2 = NativeOrWithId::Native;
|
|
|
|
create_tokens(user, vec![token_1.clone()]);
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 1, user, 1000));
|
|
|
|
let liquidity1 = 1000;
|
|
let liquidity2 = 20;
|
|
assert_noop!(
|
|
AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_1.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
),
|
|
Error::<Test>::InvalidAssetPair
|
|
);
|
|
|
|
let exchange_amount = 10;
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_1.clone(), token_1.clone()],
|
|
exchange_amount,
|
|
1,
|
|
user,
|
|
true,
|
|
),
|
|
Error::<Test>::InvalidAssetPair
|
|
);
|
|
|
|
assert_noop!(
|
|
AssetConversion::swap_exact_tokens_for_tokens(
|
|
RuntimeOrigin::signed(user),
|
|
bvec![token_2.clone(), token_2.clone()],
|
|
exchange_amount,
|
|
1,
|
|
user,
|
|
true,
|
|
),
|
|
Error::<Test>::InvalidAssetPair
|
|
);
|
|
});
|
|
}
|
|
|
|
#[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_native_ed();
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), attacker, 10000 + ed));
|
|
|
|
// The target pool the user wants to create is Native <=> WithId(2)
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
// Attacker computes the still non-existing pool account for the target pair
|
|
let pool_account =
|
|
<Test as Config>::PoolLocator::address(&(token_1.clone(), token_2.clone())).unwrap();
|
|
// And transfers the ED to that pool account
|
|
assert_ok!(Balances::transfer_allow_death(
|
|
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![NativeOrWithId::WithId(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.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
// User has to transfer one WithId(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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
10000,
|
|
100,
|
|
10000,
|
|
10,
|
|
user,
|
|
));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn swap_transactional() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let user2 = 2;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
let token_3 = NativeOrWithId::WithId(3);
|
|
|
|
let asset_ed = 150;
|
|
create_tokens_with_ed(user, vec![token_2.clone(), token_3.clone()], asset_ed);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_3.clone())
|
|
));
|
|
|
|
let ed = get_native_ed();
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, 1000));
|
|
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, 1000));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user2, 1000));
|
|
|
|
let liquidity1 = 10000;
|
|
let liquidity2 = 200;
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_3.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
let pool_1 =
|
|
<Test as Config>::PoolLocator::address(&(token_1.clone(), token_2.clone())).unwrap();
|
|
let pool_2 =
|
|
<Test as Config>::PoolLocator::address(&(token_1.clone(), token_3.clone())).unwrap();
|
|
|
|
assert_eq!(Balances::balance(&pool_1), liquidity1);
|
|
assert_eq!(Assets::balance(2, pool_1), liquidity2);
|
|
assert_eq!(Balances::balance(&pool_2), liquidity1);
|
|
assert_eq!(Assets::balance(3, pool_2), liquidity2);
|
|
|
|
// the amount that would cause a transfer from the last pool in the path to fail
|
|
let expected_out = liquidity2 - asset_ed + 1;
|
|
let amount_in = AssetConversion::balance_path_from_amount_out(
|
|
expected_out,
|
|
vec![token_2.clone(), token_1.clone(), token_3.clone()],
|
|
)
|
|
.unwrap()
|
|
.first()
|
|
.map(|(_, a)| *a)
|
|
.unwrap();
|
|
|
|
// swap credit with `swap_tokens_for_exact_tokens` transactional
|
|
let credit_in = NativeAndAssets::issue(token_2.clone(), amount_in);
|
|
let credit_in_err_expected = NativeAndAssets::issue(token_2.clone(), amount_in);
|
|
// avoiding drop of any credit, to assert any storage mutation from an actual call.
|
|
let error;
|
|
assert_storage_noop!(
|
|
error = <AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(
|
|
vec![token_2.clone(), token_1.clone(), token_3.clone()],
|
|
credit_in,
|
|
expected_out,
|
|
)
|
|
.unwrap_err()
|
|
);
|
|
assert_eq!(error, (credit_in_err_expected, TokenError::NotExpendable.into()));
|
|
|
|
// swap credit with `swap_exact_tokens_for_tokens` transactional
|
|
let credit_in = NativeAndAssets::issue(token_2.clone(), amount_in);
|
|
let credit_in_err_expected = NativeAndAssets::issue(token_2.clone(), amount_in);
|
|
// avoiding drop of any credit, to assert any storage mutation from an actual call.
|
|
let error;
|
|
assert_storage_noop!(
|
|
error = <AssetConversion as SwapCredit<_>>::swap_exact_tokens_for_tokens(
|
|
vec![token_2.clone(), token_1.clone(), token_3.clone()],
|
|
credit_in,
|
|
Some(expected_out),
|
|
)
|
|
.unwrap_err()
|
|
);
|
|
assert_eq!(error, (credit_in_err_expected, TokenError::NotExpendable.into()));
|
|
|
|
// swap with `swap_exact_tokens_for_tokens` transactional
|
|
assert_noop!(
|
|
<AssetConversion as Swap<_>>::swap_exact_tokens_for_tokens(
|
|
user2,
|
|
vec![token_2.clone(), token_1.clone(), token_3.clone()],
|
|
amount_in,
|
|
Some(expected_out),
|
|
user2,
|
|
true,
|
|
),
|
|
TokenError::NotExpendable
|
|
);
|
|
|
|
// swap with `swap_exact_tokens_for_tokens` transactional
|
|
assert_noop!(
|
|
<AssetConversion as Swap<_>>::swap_tokens_for_exact_tokens(
|
|
user2,
|
|
vec![token_2.clone(), token_1.clone(), token_3.clone()],
|
|
expected_out,
|
|
Some(amount_in),
|
|
user2,
|
|
true,
|
|
),
|
|
TokenError::NotExpendable
|
|
);
|
|
|
|
assert_eq!(Balances::balance(&pool_1), liquidity1);
|
|
assert_eq!(Assets::balance(2, pool_1), liquidity2);
|
|
assert_eq!(Balances::balance(&pool_2), liquidity1);
|
|
assert_eq!(Assets::balance(3, pool_2), liquidity2);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn swap_credit_returns_change() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_ed();
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + 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),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
let expected_change = NativeAndAssets::issue(token_1.clone(), 100);
|
|
let expected_credit_out = NativeAndAssets::issue(token_2.clone(), 20);
|
|
|
|
let amount_in_max =
|
|
AssetConversion::get_amount_in(&expected_credit_out.peek(), &liquidity1, &liquidity2)
|
|
.unwrap();
|
|
|
|
let credit_in =
|
|
NativeAndAssets::issue(token_1.clone(), amount_in_max + expected_change.peek());
|
|
assert_ok!(
|
|
<AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(
|
|
vec![token_1.clone(), token_2.clone()],
|
|
credit_in,
|
|
expected_credit_out.peek(),
|
|
),
|
|
(expected_credit_out, expected_change)
|
|
);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn swap_credit_insufficient_amount_bounds() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let user2 = 2;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_ed();
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000));
|
|
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, 1000));
|
|
|
|
let liquidity1 = 10000;
|
|
let liquidity2 = 200;
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
// provided `credit_in` is not sufficient to swap for desired `amount_out_min`
|
|
let amount_out_min = 20;
|
|
let amount_in =
|
|
AssetConversion::get_amount_in(&(amount_out_min - 1), &liquidity2, &liquidity1)
|
|
.unwrap();
|
|
let credit_in = NativeAndAssets::issue(token_1.clone(), amount_in);
|
|
let expected_credit_in = NativeAndAssets::issue(token_1.clone(), amount_in);
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_exact_tokens_for_tokens(
|
|
vec![token_1.clone(), token_2.clone()],
|
|
credit_in,
|
|
Some(amount_out_min),
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(
|
|
error,
|
|
(expected_credit_in, Error::<Test>::ProvidedMinimumNotSufficientForSwap.into())
|
|
);
|
|
|
|
// provided `credit_in` is not sufficient to swap for desired `amount_out`
|
|
let amount_out = 20;
|
|
let amount_in_max =
|
|
AssetConversion::get_amount_in(&(amount_out - 1), &liquidity2, &liquidity1).unwrap();
|
|
let credit_in = NativeAndAssets::issue(token_1.clone(), amount_in_max);
|
|
let expected_credit_in = NativeAndAssets::issue(token_1.clone(), amount_in_max);
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(
|
|
vec![token_1.clone(), token_2.clone()],
|
|
credit_in,
|
|
amount_out,
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(
|
|
error,
|
|
(expected_credit_in, Error::<Test>::ProvidedMaximumNotSufficientForSwap.into())
|
|
);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn swap_credit_zero_amount() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let user2 = 2;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_ed();
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000));
|
|
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, 1000));
|
|
|
|
let liquidity1 = 10000;
|
|
let liquidity2 = 200;
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
// swap with zero credit fails for `swap_exact_tokens_for_tokens`
|
|
let credit_in = CreditOf::<Test>::zero(token_1.clone());
|
|
let expected_credit_in = CreditOf::<Test>::zero(token_1.clone());
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_exact_tokens_for_tokens(
|
|
vec![token_1.clone(), token_2.clone()],
|
|
credit_in,
|
|
None,
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error, (expected_credit_in, Error::<Test>::ZeroAmount.into()));
|
|
|
|
// swap with zero credit fails for `swap_tokens_for_exact_tokens`
|
|
let credit_in = CreditOf::<Test>::zero(token_1.clone());
|
|
let expected_credit_in = CreditOf::<Test>::zero(token_1.clone());
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(
|
|
vec![token_1.clone(), token_2.clone()],
|
|
credit_in,
|
|
10,
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error, (expected_credit_in, Error::<Test>::ZeroAmount.into()));
|
|
|
|
// swap with zero amount_out_min fails for `swap_exact_tokens_for_tokens`
|
|
let credit_in = NativeAndAssets::issue(token_1.clone(), 10);
|
|
let expected_credit_in = NativeAndAssets::issue(token_1.clone(), 10);
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_exact_tokens_for_tokens(
|
|
vec![token_1.clone(), token_2.clone()],
|
|
credit_in,
|
|
Some(0),
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error, (expected_credit_in, Error::<Test>::ZeroAmount.into()));
|
|
|
|
// swap with zero amount_out fails with `swap_tokens_for_exact_tokens` fails
|
|
let credit_in = NativeAndAssets::issue(token_1.clone(), 10);
|
|
let expected_credit_in = NativeAndAssets::issue(token_1.clone(), 10);
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(
|
|
vec![token_1.clone(), token_2.clone()],
|
|
credit_in,
|
|
0,
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error, (expected_credit_in, Error::<Test>::ZeroAmount.into()));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn swap_credit_invalid_path() {
|
|
new_test_ext().execute_with(|| {
|
|
let user = 1;
|
|
let user2 = 2;
|
|
let token_1 = NativeOrWithId::Native;
|
|
let token_2 = NativeOrWithId::WithId(2);
|
|
|
|
create_tokens(user, vec![token_2.clone()]);
|
|
assert_ok!(AssetConversion::create_pool(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone())
|
|
));
|
|
|
|
let ed = get_native_ed();
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000));
|
|
|
|
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed));
|
|
assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, 1000));
|
|
|
|
let liquidity1 = 10000;
|
|
let liquidity2 = 200;
|
|
|
|
assert_ok!(AssetConversion::add_liquidity(
|
|
RuntimeOrigin::signed(user),
|
|
Box::new(token_1.clone()),
|
|
Box::new(token_2.clone()),
|
|
liquidity1,
|
|
liquidity2,
|
|
1,
|
|
1,
|
|
user,
|
|
));
|
|
|
|
// swap with credit_in.asset different from path[0] asset fails
|
|
let credit_in = NativeAndAssets::issue(token_1.clone(), 10);
|
|
let expected_credit_in = NativeAndAssets::issue(token_1.clone(), 10);
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_exact_tokens_for_tokens(
|
|
vec![token_2.clone(), token_1.clone()],
|
|
credit_in,
|
|
None,
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error, (expected_credit_in, Error::<Test>::InvalidPath.into()));
|
|
|
|
// swap with credit_in.asset different from path[0] asset fails
|
|
let credit_in = NativeAndAssets::issue(token_2.clone(), 10);
|
|
let expected_credit_in = NativeAndAssets::issue(token_2.clone(), 10);
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(
|
|
vec![token_1.clone(), token_2.clone()],
|
|
credit_in,
|
|
10,
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error, (expected_credit_in, Error::<Test>::InvalidPath.into()));
|
|
|
|
// swap with path.len < 2 fails
|
|
let credit_in = NativeAndAssets::issue(token_1.clone(), 10);
|
|
let expected_credit_in = NativeAndAssets::issue(token_1.clone(), 10);
|
|
let error = <AssetConversion as SwapCredit<_>>::swap_exact_tokens_for_tokens(
|
|
vec![token_2.clone()],
|
|
credit_in,
|
|
None,
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error, (expected_credit_in, Error::<Test>::InvalidPath.into()));
|
|
|
|
// swap with path.len < 2 fails
|
|
let credit_in = NativeAndAssets::issue(token_2.clone(), 10);
|
|
let expected_credit_in = NativeAndAssets::issue(token_2.clone(), 10);
|
|
let error =
|
|
<AssetConversion as SwapCredit<_>>::swap_tokens_for_exact_tokens(vec![], credit_in, 10)
|
|
.unwrap_err();
|
|
assert_eq!(error, (expected_credit_in, Error::<Test>::InvalidPath.into()));
|
|
});
|
|
}
|