diff --git a/pallets/pallet-bonded-coins/src/lib.rs b/pallets/pallet-bonded-coins/src/lib.rs index 11489bc7ee..1ecf49bc0c 100644 --- a/pallets/pallet-bonded-coins/src/lib.rs +++ b/pallets/pallet-bonded-coins/src/lib.rs @@ -289,6 +289,10 @@ pub mod pallet { let pool_account = &pool_id.clone().into(); + // Touch the pool account in order to be able to transfer the collateral + // currency to it. This should also verify that the currency actually exists. + T::CollateralCurrencies::touch(collateral_id.clone(), pool_account, &who)?; + currencies .into_iter() .zip(currency_ids.iter()) @@ -313,10 +317,6 @@ pub mod pallet { Ok(()) })?; - // Touch the pool account in order to be able to transfer the collateral - // currency to it. This should also verify that the currency actually exists. - T::CollateralCurrencies::touch(collateral_id.clone(), pool_account, &who)?; - Pools::::set( &pool_id, Some(PoolDetails::new( @@ -961,13 +961,13 @@ pub mod pallet { Ok((currency_array, start_id)) } - fn get_currencies_number(pool_details: &PoolDetailsOf) -> u32 { + pub(crate) fn get_currencies_number(pool_details: &PoolDetailsOf) -> u32 { // bonded_currencies is a BoundedVec with maximum length MaxCurrencies, which is // a u32; conversion to u32 must thus be lossless. pool_details.bonded_currencies.len().saturated_into() } - fn calculate_pool_deposit>>( + pub(crate) fn calculate_pool_deposit>>( n_currencies: N, ) -> DepositCurrencyBalanceOf { T::BaseDeposit::get() diff --git a/pallets/pallet-bonded-coins/src/mock.rs b/pallets/pallet-bonded-coins/src/mock.rs index d1fcebd5ec..df56db37be 100644 --- a/pallets/pallet-bonded-coins/src/mock.rs +++ b/pallets/pallet-bonded-coins/src/mock.rs @@ -38,10 +38,11 @@ pub(crate) const ACCOUNT_00: AccountId = AccountId::new([0u8; 32]); pub(crate) const ACCOUNT_01: AccountId = AccountId::new([1u8; 32]); const ACCOUNT_99: AccountId = AccountId::new([99u8; 32]); // assets -pub(crate) const DEFAULT_BONDED_CURRENCY_ID: AssetId = 0; -pub(crate) const DEFAULT_COLLATERAL_CURRENCY_ID: AssetId = AssetId::MAX; +pub(crate) const DEFAULT_BONDED_CURRENCY_ID: AssetId = 1; +pub(crate) const DEFAULT_COLLATERAL_CURRENCY_ID: AssetId = 0; pub(crate) const DEFAULT_COLLATERAL_DENOMINATION: u8 = 10; pub(crate) const DEFAULT_BONDED_DENOMINATION: u8 = 10; +pub(crate) const ONE_HUNDRED_KILT: u128 = 100_000_000_000_000_000; // helper functions pub fn assert_relative_eq(target: Float, expected: Float, epsilon: Float) { @@ -98,7 +99,7 @@ pub mod runtime { bonded_currencies, state, collateral_id, - denomination: 10, + denomination: DEFAULT_BONDED_DENOMINATION, owner, } } @@ -276,19 +277,24 @@ pub mod runtime { let collateral_assets = self.collaterals.into_iter().map(|id| (id, ACCOUNT_99, false, 1)); - pallet_assets::GenesisConfig:: { - assets: self - .pools - .iter() - .flat_map(|(owner, pool)| { - pool.bonded_currencies - .iter() - .map(|id| (*id, owner.to_owned(), false, 1u128)) - .collect::>() - }) - .chain(collateral_assets) - .collect(), + let all_assets: Vec<_> = self + .pools + .iter() + .flat_map(|(owner, pool)| { + pool.bonded_currencies + .iter() + .map(|id| (*id, owner.to_owned(), false, 1u128)) + .collect::>() + }) + .chain(collateral_assets) + .collect(); + + // NextAssetId is set to the maximum value of all collateral/bonded currency ids, plus one. + // If no currencies are created, it's set to 0. + let next_asset_id = all_assets.iter().map(|(id, ..)| id).max().map_or(0, |id| id + 1); + pallet_assets::GenesisConfig:: { + assets: all_assets, accounts: self.bonded_balance, metadata: self .pools @@ -313,6 +319,8 @@ pub mod runtime { self.pools.into_iter().for_each(|(pool_id, pool)| { crate::Pools::::insert(pool_id, pool); }); + + crate::NextAssetId::::set(next_asset_id); }); ext diff --git a/pallets/pallet-bonded-coins/src/tests/mod.rs b/pallets/pallet-bonded-coins/src/tests/mod.rs index 37b9348257..35baf87951 100644 --- a/pallets/pallet-bonded-coins/src/tests/mod.rs +++ b/pallets/pallet-bonded-coins/src/tests/mod.rs @@ -1 +1,2 @@ mod curves; +mod transactions; diff --git a/pallets/pallet-bonded-coins/src/tests/transactions/create_pool.rs b/pallets/pallet-bonded-coins/src/tests/transactions/create_pool.rs index 8b13789179..bc1d3adb2e 100644 --- a/pallets/pallet-bonded-coins/src/tests/transactions/create_pool.rs +++ b/pallets/pallet-bonded-coins/src/tests/transactions/create_pool.rs @@ -1 +1,301 @@ +use frame_support::{ + assert_err, assert_ok, + traits::fungibles::{ + metadata::Inspect as InspectMetadata, roles::Inspect as InspectRoles, Inspect as InspectFungibles, + }, +}; +use frame_system::{pallet_prelude::OriginFor, RawOrigin}; +use pallet_assets::Error as AssetsPalletErrors; +use sp_runtime::{ArithmeticError, BoundedVec}; +use sp_std::ops::Sub; +use crate::{ + mock::{runtime::*, *}, + types::{Locks, PoolStatus}, + Event as BondingPalletEvents, NextAssetId, Pools, TokenMetaOf, +}; + +#[test] +fn single_currency() { + let initial_balance = ONE_HUNDRED_KILT; + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_00, initial_balance)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build() + .execute_with(|| { + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_token = TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"btc".to_vec()), + min_balance: 1, + }; + + let new_asset_id = NextAssetId::::get(); + + assert_ok!(BondingPallet::create_pool( + origin, + curve, + DEFAULT_COLLATERAL_CURRENCY_ID, + BoundedVec::truncate_from(vec![bonded_token]), + DEFAULT_BONDED_DENOMINATION, + true + )); + + let pool_id = calculate_pool_id(&[new_asset_id]); + + let details = Pools::::get(&pool_id).unwrap(); + + assert!(details.is_owner(&ACCOUNT_00)); + assert!(details.is_manager(&ACCOUNT_00)); + assert!(details.transferable); + assert_eq!( + details.state, + PoolStatus::Locked(Locks { + allow_mint: false, + allow_burn: false, + allow_swap: false + }) + ); + assert_eq!(details.denomination, DEFAULT_BONDED_DENOMINATION); + assert_eq!(details.collateral_id, DEFAULT_COLLATERAL_CURRENCY_ID); + assert_eq!(details.bonded_currencies, vec![new_asset_id]); + + // collateral is id 0, new bonded currency should be 1, next is 2 + assert_eq!(NextAssetId::::get(), new_asset_id + 1); + + assert_eq!( + Balances::free_balance(ACCOUNT_00), + initial_balance.sub(BondingPallet::calculate_pool_deposit(1)) + ); + + System::assert_has_event(BondingPalletEvents::PoolCreated { id: pool_id.clone() }.into()); + + // Check creation + assert!(::Fungibles::asset_exists(new_asset_id)); + // Check team + assert_eq!( + ::Fungibles::owner(new_asset_id), + Some(pool_id.clone()) + ); + assert_eq!( + ::Fungibles::admin(new_asset_id), + Some(pool_id.clone()) + ); + assert_eq!( + ::Fungibles::issuer(new_asset_id), + Some(pool_id.clone()) + ); + assert_eq!( + ::Fungibles::freezer(new_asset_id), + Some(pool_id.clone()) + ); + // Check metadata + assert_eq!( + ::Fungibles::decimals(new_asset_id), + DEFAULT_BONDED_DENOMINATION + ); + assert_eq!(::Fungibles::name(new_asset_id), b"Bitcoin"); + assert_eq!(::Fungibles::symbol(new_asset_id), b"btc"); + }); +} + +#[test] +fn multi_currency() { + let initial_balance = ONE_HUNDRED_KILT; + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_00, initial_balance)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build() + .execute_with(|| { + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_token = TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"btc".to_vec()), + min_balance: 1, + }; + + let bonded_tokens = vec![bonded_token; 3]; + + let next_asset_id = NextAssetId::::get(); + + assert_ok!(BondingPallet::create_pool( + origin, + curve, + DEFAULT_COLLATERAL_CURRENCY_ID, + BoundedVec::truncate_from(bonded_tokens), + DEFAULT_BONDED_DENOMINATION, + true + )); + + assert_eq!(NextAssetId::::get(), next_asset_id + 3); + + let new_assets = Vec::from_iter(next_asset_id..next_asset_id + 3); + let pool_id = calculate_pool_id(&new_assets); + + let details = Pools::::get(pool_id.clone()).unwrap(); + + assert_eq!(BondingPallet::get_currencies_number(&details), 3); + assert_eq!(details.bonded_currencies, new_assets); + + assert_eq!( + Balances::free_balance(ACCOUNT_00), + initial_balance.sub(BondingPallet::calculate_pool_deposit(3)) + ); + + for new_asset_id in new_assets { + assert!(::Fungibles::asset_exists(new_asset_id)); + assert_eq!( + ::Fungibles::owner(new_asset_id), + Some(pool_id.clone()) + ); + } + }); +} + +#[test] +fn can_create_identical_pools() { + let initial_balance = ONE_HUNDRED_KILT; + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_00, initial_balance)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build() + .execute_with(|| { + let origin: OriginFor = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_token = TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"btc".to_vec()), + min_balance: 1, + }; + + let next_asset_id = NextAssetId::::get(); + + assert_ok!(BondingPallet::create_pool( + origin.clone(), + curve.clone(), + DEFAULT_COLLATERAL_CURRENCY_ID, + BoundedVec::truncate_from(vec![bonded_token.clone()]), + DEFAULT_BONDED_DENOMINATION, + true + )); + + assert_ok!(BondingPallet::create_pool( + origin, + curve, + DEFAULT_COLLATERAL_CURRENCY_ID, + BoundedVec::truncate_from(vec![bonded_token]), + DEFAULT_BONDED_DENOMINATION, + true + )); + + assert_eq!(NextAssetId::::get(), next_asset_id + 2); + + let details1 = Pools::::get(calculate_pool_id(&[next_asset_id])).unwrap(); + let details2 = Pools::::get(calculate_pool_id(&[next_asset_id + 1])).unwrap(); + + assert_eq!(details1.bonded_currencies, vec![next_asset_id]); + assert_eq!(details2.bonded_currencies, vec![next_asset_id + 1]); + + assert!(::Fungibles::asset_exists(next_asset_id)); + assert!(::Fungibles::asset_exists(next_asset_id + 1)); + }); +} + +#[test] +fn fails_if_collateral_not_exists() { + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_00, ONE_HUNDRED_KILT)]) + .build() + .execute_with(|| { + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_token = TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"btc".to_vec()), + min_balance: 1, + }; + + assert_err!( + BondingPallet::create_pool( + origin, + curve, + 100, + BoundedVec::truncate_from(vec![bonded_token]), + DEFAULT_BONDED_DENOMINATION, + true + ), + AssetsPalletErrors::::Unknown + ); + }) +} + +#[test] +fn cannot_create_circular_pool() { + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_00, ONE_HUNDRED_KILT)]) + .build() + .execute_with(|| { + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_token = TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"btc".to_vec()), + min_balance: 1, + }; + + let next_asset_id = NextAssetId::::get(); + + assert_err!( + BondingPallet::create_pool( + origin, + curve, + // try specifying the id of the currency to be created as collateral + next_asset_id, + BoundedVec::truncate_from(vec![bonded_token]), + DEFAULT_BONDED_DENOMINATION, + true + ), + AssetsPalletErrors::::Unknown + ); + }) +} + +#[test] +fn handles_asset_id_overflow() { + let initial_balance = ONE_HUNDRED_KILT; + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_00, initial_balance)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build() + .execute_with(|| { + NextAssetId::::set(u32::MAX); + + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_token = TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"btc".to_vec()), + min_balance: 1, + }; + + assert_err!( + BondingPallet::create_pool( + origin, + curve, + 0, + BoundedVec::truncate_from(vec![bonded_token; 2]), + 10, + true + ), + ArithmeticError::Overflow + ); + }); +} diff --git a/pallets/pallet-bonded-coins/src/tests/transactions/mod.rs b/pallets/pallet-bonded-coins/src/tests/transactions/mod.rs index 1e13361980..ca68349384 100644 --- a/pallets/pallet-bonded-coins/src/tests/transactions/mod.rs +++ b/pallets/pallet-bonded-coins/src/tests/transactions/mod.rs @@ -3,5 +3,4 @@ mod create_pool; mod mint_into; mod set_lock; mod swap_into; - mod unlock; diff --git a/pallets/pallet-bonded-coins/src/traits.rs b/pallets/pallet-bonded-coins/src/traits.rs index 8f35477ff9..ee442f3823 100644 --- a/pallets/pallet-bonded-coins/src/traits.rs +++ b/pallets/pallet-bonded-coins/src/traits.rs @@ -13,7 +13,6 @@ pub trait FreezeAccounts { fn thaw(asset_id: &AssetId, who: &AccountId) -> Result<(), Self::Error>; } - type AssetIdOf = ::AssetId; impl FreezeAccounts, ::AssetId> for AssetsPallet @@ -39,9 +38,8 @@ where } } - /// Copy from the Polkadot SDK. once we are at version 1.13.0, we can remove -/// this. +/// this. pub trait ResetTeam: Inspect { /// Reset the team for the asset with the given `id`. ///