diff --git a/pallets/pallet-bonded-coins/README.md b/pallets/pallet-bonded-coins/README.md index 9bfd466f8..66390d719 100644 --- a/pallets/pallet-bonded-coins/README.md +++ b/pallets/pallet-bonded-coins/README.md @@ -11,13 +11,6 @@ This pallet provides functionality to: - Burn tokens to release collateral. - Manage the lifecycle of currency pools, including refunding and destroying pools. -### Rounding - -Rounding issues are a problem and cannot be completely avoided due to the nature of limited resources on a computer, resulting in a lack of representation for irrational numbers. -This pallet cannot guarantee mathematically exact calculations. -However, it can guarantee the reproducibility of the same result based on the usage of [fixed-point][fixed-point] numbers. - - ## Key Concepts ### Bonding Curve @@ -115,57 +108,214 @@ The `Config` trait defines the configuration parameters and associated types req ## Life Cycle of a Pool -1. **Creation**: - - A pool is created using the `create_pool` function. - - The manager specifies the bonding curve, collateral type, currencies, and whether the bonded coins are transferable. - - A deposit is taken from the caller, which can be reclaimed once the pool is destroyed. - -2. **Refund Process**: - - The refund process can be started by the manager using the `start_refund` function. - - The refund process can be forced to start using the `force_start_refund` function, which requires force privileges. - - Collateral can be refunded to a specific account using the `refund_account` function, based on the owned bonded currency. - -3. **Destruction**: - - The destruction process can be started by the manager using the `start_destroy` function. This operation will fail if accounts with bonded currencies still exist. - - The destruction process can be forced to start using the `force_start_destroy` function, which requires force privileges. - - The destruction process is completed using the `finish_destroy` function, which refunds any taken deposits. +1. An __Owner__ initializes a new pool by calling `create_pool`, specifying the bonding curve, metadata of the new currencies, which currency to use as a collateral, and whether currencies should be `transferable`. The __Owner__ will be the new pool’s first __Manager__. + - A storage deposit will be paid by the __Owner__ in addition to transaction fees. +2. Optional: __Owner__ makes manager-level changes to the new pool or associated assets, such as setting locks on mint/burn functionality, or changing the asset management team. +3. Optional: __Owner__ re-assigns or un-assigns management privileges. In the second case, no further management-level changes can be made, including the initialization of the refund mechanism. +4. __Traders__ buy into one of the associated currencies by calling `mint_into`. + - If the __Owner__ has flagged the pool’s associated assets as `transferable` upon creation, __Traders__ may transfer their holdings to other accounts, enabling, for example, secondary markets. This is done by interacting with the assets pallet directly via its extrinsics (`transfer`, `transfer_keep_alive`, `approve_transfer`, etc). +5. __Traders__ sell their holdings of any of the associated currencies by calling `burn_into`. +6. Optional: __Manager__ can end trading of the associated assets and distribute all collateral collected among holders. To do so, they call `start_refund`. All minting and burning is halted. + - This is followed by calling `refund_account` for each asset and account holding funds. This call can be called by anyone for any account. +7. When no collateral remains in the pool _OR_ when all linked assets have a total supply of 0 (all funds burnt), the pool __Owner__ or __Manager__ can initialize the destruction of the pool by calling start_destroy. All minting, burning, transferring, and refunding is halted. + - If the pool __Manager__ has been unassigned, the pool cannot be drained forcefully by the __Owner__. Either all __Traders__ need to be convinced to burn their holdings, or an appeal must be made to the configured force origin (typically blockchain governance) to call `force_start_refund` (enabling collateral distribution as in 6.) or `force_start_destroy` (forcefully destroying the pool despite value still being locked in the pool). +8. If any balance remains for some account on any asset associated with the pool, these accounts have to be destroyed by calling the asset pallet’s `destroy_accounts` extrinsic for that asset. + - This can be called by any account. + - This scenario may occur either because destruction was initiated via `force_start_destroy`, or in rare cases where during refunding there is less collateral than bonded currency and not all accounts receive a share of collateral, leaving the collateral exhausted before all accounts have been refunded. +9. If any approvals have been created for some account by __Traders__ (via calling `approve_transfer` on the assets pallet) on any asset associated with the pool, these approvals have to be destroyed by calling the asset pallet’s `destroy_approvals` extrinsic for that asset. + - This can be called by any account. +10. Once no accounts or approvals remain on any associated asset, the pool record can be purged from the blockchain state by calling `finish_destroy`. + - The storage deposit and (if any) residual collateral will be transferred to the __Owner__. + - This can be called by any account. ## Functions ### Public Functions +#### Permissionless + +Can be called by any regular origin (`DefaultOrigin`/`PoolCreateOrigin`; subject to runtime configuration). + - `create_pool`: Creates a new pool with the specified bonding curve, collateral type, and currencies. - During the pool creation step, the manager must specify whether the bonded coins are transferable or not. - This flag can not be changed later on. - The selected denomination is used not only as metadata but also to scale down the existing supply or the amount specified in a mint or burn operation. + Additional configuration determines the denomination of bonded coins, whether they are + transferable or not, and whether their asset management teams may be re-assigned. + These settings cannot be changed after pool creation. + The selected denomination is used not only as metadata but also to scale down the existing supply or the amount specified in a mint or burn operation. A deposit is taken from the caller, which is returned once the pool is destroyed. -- `reset_team`: Resets the managing team of a pool. - Only the admin and the freezer can be updated in this operation. -- `reset_manager`: Resets the manager of a pool. -- `set_lock`: Sets a lock on a pool. - Locks specify who is able to mint and burn a bonded currency. - After applying the lock, the pool becomes permissioned, and only the manager is able to mint or burn bonded currencies. -- `unlock`: Unlocks the pool. - Can only be called by the manager. - `mint_into`: Mints new tokens by locking up collateral. In the mint_into operation the beneficiary must be specified. The collateral is taken from the caller. - `burn_into`: Burns tokens to release collateral. In the burn_into operation the beneficiary must be specified. The funds are burned from the caller. -- `start_refund`: Starts the refund process for a pool. - Only the manager is able to start the refund process. -- `force_start_refund`: Forces the start of the refund process for a pool. - Requires force privileges. -- `refund_account`: Refunds collateral to a specific account. +- `refund_account`: Can only be called on pools in 'Refunding' state. Refunds collateral to a specific account. The amount of refunded collateral is determined by the owned bonded currency. +- `finish_destroy`: Can only be called on pools in 'Destroying' state. Completes the destruction process for a pool. + Refunds any taken deposits. + +#### Permissioned + +Can only be called by a pool's manager origin. + +- `reset_team`: Resets the managing team of a pool. + Only the admin and the freezer can be updated in this operation. + This will always fail for pools where the `allow_reset_team` setting is `false`. +- `reset_manager`: Resets the manager of a pool. +- `set_lock`: Sets a lock on a pool. + Locks specify who is able to mint and burn a bonded currency. + After applying the lock, the pool becomes permissioned, and only the manager is able to mint or burn bonded currencies. +- `unlock`: Unlocks the pool. +- `start_refund`: Starts the refund process for a pool. - `start_destroy`: Starts the destruction process for a pool. - Only the manager and the owner is able to start the destroy process. + Both the manager and the owner are able to start the destroy process. If accounts with bonded currencies still exist, this operation will fail. + +#### Privileged + +Can only be called by the force origin (`ForceOrigin`; subject to runtime configuration). + +- `force_start_refund`: Forces the start of the refund process for a pool. + Requires force privileges. - `force_start_destroy`: Forces the start of the destruction process for a pool. Requires force privileges. -- `finish_destroy`: Completes the destruction process for a pool. - Refunds any taken deposits. + +## Permissions Structure & (De-)Centralization + +In its current form, the pallet allows three different levels of central control over a pool: + +#### Full manager authority + +Upon creation, every pool has the `manager` field set to the account creating the pool (the __Owner__). The __Owner__ can also choose to set the `allow_reset_team` flag on the pool to `true` during creation (this cannot be changed afterwards\!). In this configuration, the __Owner__/__Manager__ has near-total authority over the pool’s lifecycle and its associated assets, allowing them to: + +- Transfer pool management privileges to another account +- Impose and lift a Lock on the pool, halting/resuming all minting and/or burning +- Mint and burn bonded tokens even though mint/burn is locked for all others +- Initiate the refund process, halting bonded token mints and burns + - Since the `refund_account` extrinsic applies for refunds proportionally across all bonded currencies, assuming equal value, the refund process could potentially be abused to perform market manipulations. +- Assign or modify the currency management team, and thereby exercise control over bonded currency funds held by other wallets, including: + - Freeze or unfreeze funds + - Force-transfer a wallet’s balance to another account + - Slash/destroy a wallet’s balance + +For these reasons users are advised to exercise caution when a pool’s `allow_reset_team` is set to `true`. **This is true even if the `manager` field is set to `None`**; even though the pool configuration is now immutable, asset management team changes may have been made prior to un-assigning the __Manager__, which means that individuals may still exercise privileged control over the bonded assets linked to the pool. + +#### Restricted manager authority + +Pools where the `allow_reset_team` is set to `false` do not allow changes to the asset management team, even if a pool `manager` exists. Therefore, the __Manager__ has a much more restricted set of privileges, limited to: + +- Transferring pool management privileges to another account +- Imposing and lifting a Lock on the pool, halting/resuming all minting and/or burning +- Minting and burning bonded tokens even though mint/burn is locked for all others +- Initiating the refund process, halting bonded token mints and burns + +Still, these permissions can potentially allow a __Manager__ to: + +- Trap user’s funds by imposing burn locks after they minted coins in exchange for collateral +- Perform market manipulations via imposing locks or via initiating refund when token distribution and price conditions are in their favor + +#### Unmanaged / unprivileged pools + +While the `manager` field is set to the pool creator at the time of creation, a __Manager__ can drop all privileges by calling the `reset_manager` transaction with a `None` argument, resulting in the `manager` field being unset. +Pools with no __Manager__ and the `allow_reset_team` set to `false` can be considered unprivileged, as: + +- Their current configuration is immutable +- The pool __Owner__ has given up all relevant privileges and control over the pool’s bonded assets + +While there is still a single transaction that only the __Owner__ is privileged to make (`start_destroy`), this does not significantly impact bonded token economics, as it can only be called once all users have burnt all their holdings of bonded tokens linked to this pool. +Unprivileged pools where bonded currency supplies are non-zero thus cannot be purged unless by force origin intervention. + +### Force Origin + +The pallet implements several transactions, prefixed with `force_*`, that are restricted to use by a ‘force’-origin. This origin can be configured differently in each chain/runtime this pallet is integrated into, but is assumed to be the blockchain’s governing body which can also decide on runtime upgrades and thus has unlimited control over blockchain state and state transition functions. +The transactions `force_start_refund` & `force_start_destroy` are designed to allow the forced closure of pools, which may be necessary, e.g., because of inactive accounts preventing purge of a pool which is otherwise unused, or because the pool has been found to be involved in illegitimate activities. + +## Known Limitations + +### Rounding + +Rounding issues are a problem and cannot be completely avoided due to the nature of limited resources on a computer, resulting in a lack of representation for irrational numbers. +This pallet cannot guarantee mathematically exact calculations. +However, it can guarantee the reproducibility of the same result based on the usage of [fixed-point][fixed-point] numbers. + +### Numerical Overflow + +Working with fixed precision numerical representations imposes limits on the supply of bonded tokens that can exist, as well as on the amount that can be burnt or minted at once. These boundaries depend on multiple factors, including the data types chosen for representing balances in the pallets and in the bonding curves module, the denominations of collateral and bonded tokens, and the chosen curve and parameters. + +The following types of boundaries related to numerical overflow of token amounts exist in the system: + +#### Limits independent of curve- and parameter choice + +##### **Limited total supply due to capacity of assets pallet / fungibles balance type** + +Bonded coin balances are stored in an external pallet implementing the Fungibles interfaces defined in [`frame_support::traits::tokens::fungibles`](https://docs.rs/frame-support/35.0.0/frame_support/traits/tokens/fungibles/index.html). The size of the integer type used to represent balances limits the maximum balance that can be stored for any given bonded currency. And because the same type is used for representing the total supply of this asset, this limit applies to the sum of all balances as well. + +*Example:* +The u128 integer type frequently used to store balances in polkadot imposes a maximum supply of 2128 \= 3.40 \* 10³⁸. + +##### **Limited total supply due to capacity of bonding curves module parameter type** + +The bonding curves module uses fixed-precision fractional numbers for cost calculations. To do so, total supplies received from the Fungibles implementation are scaled by their denomination to account for the typically reduced whole-number capacity of fixed-precision fractionals compared to integer types. Depending on the choice of denomination for a pool, this limit may still be magnitudes lower than the limit imposed by the capacity of the balance (integer) type. + +*Example:* +[Using a fixed-point number with a maximum value of 274 and a denomination of 15, overflow would occur for any total supply \>= 274 \* 1015 \= 1.89 \* 10³⁷, which is still an order of magnitude below the capacity of a u128. + +#### Limits depending on curve- and parameter choice + +##### **LMSR** + +The following boundaries apply to all currencies in a pool, at all times. If these are violated either before or as a consequence of a mint or burn the operation will fail. + +supply \+ m × ln(N) \<= MAX(coefficient) +and +(min \- supply) / m \>= MIN(coefficient) +and +(supply \- max) / m \>= MIN(coefficient) + +where +*supply \= the token’s supply, scaled by its denomination* +*max \= largest supply in the pool’s set of bonded currencies* +*min \= smallest supply in the pool’s set of bonded currencies* +*m \= liquidity parameter m* +*N \= number of bonded currencies* +*MAX(coefficient) \= Maximum value for fixed precision number type; e.g., around 274 for a signed type with 75 integer bits.* +*MIN(coefficient) \= Minimum value for fixed precision number type; e.g., \-274 for a signed type with 75 integer bits.* + +##### **Polynomial** + +This type of curve sums up the supply of all bonded coins in a pool, so limits apply to their sum. + +Due to the curve’s exponential nature, minting large amounts at once can lead to very high cost, possibly overflowing the fixed precision number type. In scenarios where users hold large amounts of collateral that exceed the limit of the fixed type (as the collateral balance type is likely to have a larger capacity), minting in multiple increments may work. + +For very low amounts minted (-\> lim(0)) the following limits hold: + +total \<= MAX(coefficient) +and +3m x total^2 \+ 2n x total \+ o \<= MAX(coefficient) +and +3 x total^2 \<= MAX(coefficient) IF m \> 0 +and +2 x total \<= MAX(coefficient) IF n \> 0 + +where +*total \= the sum of all token’s supplies, scaled by their denomination* +*m \= curve coefficient m* +*n \= curve coefficient n* +*o \= curve coefficient o* +*MAX(coefficient) \= Maximum value for fixed precision number type; e.g., around 274 for a signed type with 75 integer bits.* + +##### **Square Root** + +As for the polynomial, this type of curve sums up the supply of all bonded coins in a pool, so limits apply to their sum. + +The following limits apply, beyond cost limits given by the capacity of the fixed precision type: + +total \<= MAX(coefficient) +and +total^(3/2) \<= MAX(coefficient) + +where +*total \= the sum of all token’s supplies, scaled by their denomination* +*MAX(coefficient) \= Maximum value for fixed precision number type; e.g., around 274 for a signed type with 75 integer bits.* [bonding-curve]: ./src/curves/mod.rs [pool-details]: ./src/types.rs diff --git a/pallets/pallet-bonded-coins/src/benchmarking.rs b/pallets/pallet-bonded-coins/src/benchmarking.rs index b0d7154ef..142b52d32 100644 --- a/pallets/pallet-bonded-coins/src/benchmarking.rs +++ b/pallets/pallet-bonded-coins/src/benchmarking.rs @@ -17,6 +17,7 @@ // If you feel like getting in touch with us, you can do so at use frame_benchmarking::v2::*; use frame_support::traits::fungibles::roles::Inspect as InspectRoles; +use scale_info::prelude::format; use sp_core::U256; use sp_std::{ ops::{AddAssign, BitOrAssign, ShlAssign}, @@ -27,9 +28,11 @@ use substrate_fixed::traits::{Fixed, FixedSigned, FixedUnsigned, ToFixed}; use crate::{ curves::{ lmsr::{LMSRParameters, LMSRParametersInput}, + polynomial::PolynomialParameters, square_root::{SquareRootParameters, SquareRootParametersInput}, Curve, CurveInput, }, + types::BondedCurrenciesSettings, Call, CollateralAssetIdOf, CollateralBalanceOf, Config, CurveParameterTypeOf, FungiblesAssetIdOf, FungiblesBalanceOf, Pallet, }; @@ -63,6 +66,13 @@ where fn set_native_balance(_account: &::AccountId, _amount: u128) {} } +fn get_2nd_order_polynomial_curve() -> Curve { + let m = Float::from_num(0.01); + let n = Float::from_num(2); + let o = Float::from_num(3); + Curve::Polynomial(PolynomialParameters { m, n, o }) +} + fn get_square_root_curve() -> Curve { let m = Float::from_num(3); let n = Float::from_num(2); @@ -220,10 +230,13 @@ mod benchmarks { owner, state, collateral, - denomination, bonded_currencies: BoundedVec::truncate_from(bonded_coin_ids.clone()), - transferable: true, - min_operation_balance: 1u128.saturated_into(), + currencies_settings: BondedCurrenciesSettings { + denomination, + transferable: true, + allow_reset_team: true, + min_operation_balance: 1u128.saturated_into(), + }, deposit: Pallet::::calculate_pool_deposit(bonded_coin_ids.len()), }; Pools::::insert(&pool_id, pool_details); @@ -233,11 +246,11 @@ mod benchmarks { fn generate_token_metadata(c: u32) -> BoundedVec, T::MaxCurrenciesPerPool> { let mut token_meta = Vec::new(); - for _ in 1..=c { + for i in 1..=c { token_meta.push(TokenMetaOf:: { min_balance: 1u128.saturated_into(), - name: BoundedVec::try_from(b"BTC".to_vec()).expect("Failed to create BoundedVec"), - symbol: BoundedVec::try_from(b"BTC".to_vec()).expect("Failed to create BoundedVec"), + name: BoundedVec::try_from(format!("Coin_{}", &i).into_bytes()).expect("Failed to create BoundedVec"), + symbol: BoundedVec::try_from(format!("BTC_{}", &i).into_bytes()).expect("Failed to create BoundedVec"), }) } BoundedVec::try_from(token_meta).expect("creating bounded Vec should not fail") @@ -263,9 +276,12 @@ mod benchmarks { curve, collateral_id, currencies, - 10, - true, - 1, + BondedCurrenciesSettings { + denomination: 10, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1u128.saturated_into(), + }, ); // Verify @@ -301,9 +317,12 @@ mod benchmarks { curve, collateral_id, currencies, - 10, - true, - 1, + BondedCurrenciesSettings { + denomination: 10, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1u128.saturated_into(), + }, ); // Verify @@ -313,7 +332,7 @@ mod benchmarks { Curve::SquareRoot(_) => { assert_eq!(id, expected_pool_id); } - _ => panic!("pool.curve is not a Polynomial function"), + _ => panic!("pool.curve is not a SquareRoot function"), } } @@ -338,9 +357,12 @@ mod benchmarks { curve, collateral_id, currencies, - 10, - true, - 1, + BondedCurrenciesSettings { + denomination: 10, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1u128.saturated_into(), + }, ); // Verify @@ -350,12 +372,12 @@ mod benchmarks { Curve::Lmsr(_) => { assert_eq!(id, expected_pool_id); } - _ => panic!("pool.curve is not a Polynomial function"), + _ => panic!("pool.curve is not a LSMR curve!"), } } #[benchmark] - fn reset_team() { + fn reset_team(c: Linear<1, { T::MaxCurrenciesPerPool::get() }>) { let origin = T::DefaultOrigin::try_successful_origin().expect("creating origin should not fail"); let account_origin = origin .clone() @@ -363,17 +385,14 @@ mod benchmarks { .expect("generating account_id from origin should not fail"); make_free_for_deposit::(&account_origin); - let bonded_coin_id = T::BenchmarkHelper::calculate_bonded_asset_id(0); - create_bonded_asset::(bonded_coin_id.clone()); + let bonded_currencies = create_bonded_currencies_in_range::(c, false); let curve = get_linear_bonding_curve::>(); - let pool_id = create_pool::( - curve, - [bonded_coin_id.clone()].to_vec(), - Some(account_origin), - None, - None, - ); + let pool_id = create_pool::(curve, bonded_currencies.clone(), Some(account_origin), None, None); + + // Although these would rarely happen in practice, for benchmarking we assume + // the worst case where the owner must be changed as well + assert!(T::Fungibles::owner(bonded_currencies[0].clone()) != Some(pool_id.clone().into())); let admin: AccountIdOf = account("admin", 0, 0); let freezer: AccountIdOf = account("freezer", 0, 0); @@ -382,12 +401,24 @@ mod benchmarks { freezer: freezer.clone(), }; + let pool_id_for_call = pool_id.clone(); + + let max_currencies = T::MaxCurrenciesPerPool::get(); #[extrinsic_call] - _(origin as T::RuntimeOrigin, pool_id, fungibles_team, 0); + _( + origin as T::RuntimeOrigin, + pool_id_for_call, + fungibles_team, + max_currencies, + ); // Verify - assert_eq!(T::Fungibles::admin(bonded_coin_id.clone()), Some(admin)); - assert_eq!(T::Fungibles::freezer(bonded_coin_id), Some(freezer)); + bonded_currencies.iter().for_each(|asset_id| { + assert_eq!(T::Fungibles::admin(asset_id.clone()), Some(admin.clone())); + assert_eq!(T::Fungibles::freezer(asset_id.clone()), Some(freezer.clone())); + assert_eq!(T::Fungibles::owner(asset_id.clone()), Some(pool_id.clone().into())); + assert_eq!(T::Fungibles::issuer(asset_id.clone()), Some(pool_id.clone().into())); + }); } #[benchmark] @@ -475,7 +506,7 @@ mod benchmarks { make_free_for_deposit::(&account_origin); set_collateral_balance::(collateral_id.clone(), &account_origin, 10000u128); - let curve = get_linear_bonding_curve::>(); + let curve = get_2nd_order_polynomial_curve::>(); let bonded_currencies = create_bonded_currencies_in_range::(c, false); let pool_id = create_pool::(curve, bonded_currencies.clone(), None, None, Some(0)); @@ -605,7 +636,7 @@ mod benchmarks { let start_balance = 100u128; set_fungible_balance::(target_asset_id.clone(), &account_origin, start_balance); - let curve = get_linear_bonding_curve::>(); + let curve = get_2nd_order_polynomial_curve::>(); let pool_id = create_pool::(curve, bonded_currencies, None, None, Some(0)); let pool_account = pool_id.clone().into(); diff --git a/pallets/pallet-bonded-coins/src/curves/mod.rs b/pallets/pallet-bonded-coins/src/curves/mod.rs index 15494de2d..97f04c6c7 100644 --- a/pallets/pallet-bonded-coins/src/curves/mod.rs +++ b/pallets/pallet-bonded-coins/src/curves/mod.rs @@ -166,7 +166,7 @@ pub fn balance_to_fixed( round_kind: Round, ) -> Result where - FixedType::Bits: TryFrom, // TODO: make large integer type configurable in runtime + FixedType::Bits: TryFrom, Balance: TryInto, { let decimals = U256::from(10u8) diff --git a/pallets/pallet-bonded-coins/src/curves/polynomial.rs b/pallets/pallet-bonded-coins/src/curves/polynomial.rs index d58f87c2b..d7087c84a 100644 --- a/pallets/pallet-bonded-coins/src/curves/polynomial.rs +++ b/pallets/pallet-bonded-coins/src/curves/polynomial.rs @@ -34,13 +34,19 @@ /// ### Antiderivative /// The indefinite integral of the cost function is: /// ```text -/// C(s) = (m / 3) * s^3 + (n / 2) * s^2 + o * s +/// C(s) = (m / 3) * s^3 + (n / 2) * s^2 + o * s = M * s^3 + N * s^2 + O * s /// ``` /// Where: /// - `m` is the coefficient for the quadratic term, /// - `n` is the coefficient for the linear term, -/// - `o` is the constant term. +/// - `o` is the constant term, +/// - `M` is the coefficient of the first (cubic) term in the antiderivative, +/// - `N` is the coefficient of the second (quadratic) term in the +/// antiderivative, +/// - `O=o` is the coefficient of the third (linear) term in the antiderivative. /// +/// Coefficients of the antiderivative `N`, `M` & `O` are used to parametrize +/// functions in this module. /// /// `C(s)` represents the accumulated cost of purchasing or selling assets up to /// the current supply `s`. The integral between two supply points, `s*` @@ -81,9 +87,11 @@ use crate::PassiveSupply; /// /// For a polynomial cost function `c(s) = 3 * s^2 + 2 * s + 2` /// -/// which is resulting into the antiderivative -/// `C(s) = (3 / 3) * s^3 + (2 / 2) * s^2 + 2 * s` -/// the input parameters would be: +/// which results in the antiderivative +/// `C(s) = (3 / 3) * s^3 + (2 / 2) * s^2 + 2 * s = 1 * s^3 + 1 * s^2 + 2 * s` +/// +/// the input parameters `M`, `N` & `O` (coefficients of the antiderivative) +/// would be: /// ```rust, ignore /// PolynomialParametersInput { /// m: 1, @@ -152,27 +160,33 @@ where .checked_add(accumulated_passive_issuance) .ok_or(ArithmeticError::Overflow)?; - // Calculate high - low - let delta_x = high.checked_sub(low).ok_or(ArithmeticError::Underflow)?; - - let high_low_mul = high.checked_mul(low).ok_or(ArithmeticError::Overflow)?; - let high_square = square(high)?; - let low_square = square(low)?; - - // Factorized cubic term: (high^2 + high * low + low^2) - let cubic_term = high_square - .checked_add(high_low_mul) - .ok_or(ArithmeticError::Overflow)? - .checked_add(low_square) - .ok_or(ArithmeticError::Overflow)?; - // Calculate m * (high^2 + high * low + low^2) - let term1 = self.m.checked_mul(cubic_term).ok_or(ArithmeticError::Overflow)?; - - let high_plus_low = high.checked_add(low).ok_or(ArithmeticError::Overflow)?; + let term1 = if self.m == Coefficient::from_num(0u8) { + // if m is 0 the product is 0 + Ok(self.m) + } else { + let high_low_mul = high.checked_mul(low).ok_or(ArithmeticError::Overflow)?; + let high_square = square(high)?; + let low_square = square(low)?; + + // Factorized cubic term: (high^2 + high * low + low^2) + let cubic_term = high_square + .checked_add(high_low_mul) + .ok_or(ArithmeticError::Overflow)? + .checked_add(low_square) + .ok_or(ArithmeticError::Overflow)?; + + self.m.checked_mul(cubic_term).ok_or(ArithmeticError::Overflow) + }?; // Calculate n * (high + low) - let term2 = self.n.checked_mul(high_plus_low).ok_or(ArithmeticError::Overflow)?; + let term2 = if self.n == Coefficient::from_num(0u8) { + // if n is 0 the product is 0 + Ok(self.n) + } else { + let high_plus_low = high.checked_add(low).ok_or(ArithmeticError::Overflow)?; + self.n.checked_mul(high_plus_low).ok_or(ArithmeticError::Overflow) + }?; // Final calculation with factored (high - low) let result = term1 @@ -181,6 +195,9 @@ where .checked_add(self.o) .ok_or(ArithmeticError::Overflow)?; + // Calculate high - low + let delta_x = high.checked_sub(low).ok_or(ArithmeticError::Underflow)?; + result.checked_mul(delta_x).ok_or(ArithmeticError::Overflow) } } diff --git a/pallets/pallet-bonded-coins/src/curves/square_root.rs b/pallets/pallet-bonded-coins/src/curves/square_root.rs index 0b87fdd03..5ea9e3c7f 100644 --- a/pallets/pallet-bonded-coins/src/curves/square_root.rs +++ b/pallets/pallet-bonded-coins/src/curves/square_root.rs @@ -33,12 +33,19 @@ /// ### Antiderivative /// The indefinite integral of the cost function is: /// ```text -/// C(s) = (2/3) * m * s^(3/2) + n * s +/// C(s) = (2/3) * m * s^(3/2) + n * s = M * s^(3/2) + N * s /// ``` /// Where: /// - `s` is the supply of assets, /// - `m` is the coefficient for the square root term, -/// - `n` is the coefficient for the linear term. +/// - `n` is the coefficient for the constant term, +/// - `M` is the coefficient for the first (fractional power) term in the +/// antiderivative, +/// - `N=n` is the coefficient of the second (linear) term in the +/// antiderivative. +/// +/// Coefficients of the antiderivative `N` & `M` are used to parametrize +/// functions in this module. /// /// `C(s)` represents the total cost of purchasing or selling assets up to the /// current supply `s`. To calculate the incremental cost of a transaction, use @@ -72,14 +79,19 @@ use crate::{PassiveSupply, Precision}; /// bonding curve. This struct is used to convert the input parameters to the /// correct fixed-point type. /// -/// The input struct assumes that the coefficients are precomputed according to -/// the integral rules of the square root function./// ### Example +/// The input struct expects coefficients of terms in the antiderivative of the +/// square root function, which must be precomputed according to the integral +/// rules of the square root function. +/// +/// ### Example +/// +/// For a square root cost function `c(s) = 3 * s^1/2 + 2` /// -/// For a square root cost function `c(s) = 3 * s^1/2 + 2 +/// which results in the antiderivative +/// `C(s) = (2 / 3) * 3 * s^(3/2) + 2 * s = 2s^(3/2) + 2s` /// -/// which is resulting into the antiderivative -/// `C(s) = (6 / 3) * s^(1/2) + 2 * s` -/// the input parameters would be: +/// the input parameters `M` & `N` (coefficients of the antiderivative) would +/// be: /// ```rust, ignore /// SquareRootParametersInput { /// m: 2, diff --git a/pallets/pallet-bonded-coins/src/lib.rs b/pallets/pallet-bonded-coins/src/lib.rs index 5d7049b0d..8512b50e2 100644 --- a/pallets/pallet-bonded-coins/src/lib.rs +++ b/pallets/pallet-bonded-coins/src/lib.rs @@ -20,6 +20,8 @@ pub use pallet::*; +pub mod migrations; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; #[cfg(any(test, feature = "runtime-benchmarks"))] @@ -36,7 +38,7 @@ mod types; #[cfg(feature = "runtime-benchmarks")] pub use benchmarking::BenchmarkHelper; -pub use types::{Locks, PoolStatus, Round}; +pub use types::{BondedCurrenciesSettings, Locks, PoolStatus, Round}; pub use default_weights::WeightInfo; @@ -64,7 +66,7 @@ pub mod pallet { Create as CreateFungibles, Destroy as DestroyFungibles, Inspect as InspectFungibles, Mutate as MutateFungibles, }, - tokens::{Fortitude, Precision as WithdrawalPrecision, Preservation, Provenance}, + tokens::{DepositConsequence, Fortitude, Precision as WithdrawalPrecision, Preservation, Provenance}, AccountTouch, }, Hashable, Parameter, @@ -79,6 +81,7 @@ pub mod pallet { BoundedVec, DispatchError, TokenError, }; use sp_std::{ + collections::btree_set::BTreeSet, iter::Iterator, ops::{AddAssign, BitOrAssign, ShlAssign}, prelude::*, @@ -92,10 +95,13 @@ pub mod pallet { use crate::{ curves::{balance_to_fixed, fixed_to_balance, BondingFunction, Curve, CurveInput}, traits::{FreezeAccounts, NextAssetIds, ResetTeam}, - types::{Locks, PoolDetails, PoolManagingTeam, PoolStatus, Round, TokenMeta}, + types::{BondedCurrenciesSettings, Locks, PoolDetails, PoolManagingTeam, PoolStatus, Round, TokenMeta}, WeightInfo, }; + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + pub(crate) type AccountIdLookupOf = <::Lookup as sp_runtime::traits::StaticLookup>::Source; @@ -124,12 +130,15 @@ pub mod pallet { pub(crate) type CurveParameterInputOf = ::CurveParameterInput; + pub(crate) type BondedCurrenciesSettingsOf = BondedCurrenciesSettings>; + pub type PoolDetailsOf = PoolDetails< ::AccountId, Curve>, BoundedCurrencyVec, CollateralAssetIdOf, DepositBalanceOf, + BondedCurrenciesSettingsOf, >; /// Minimum required amount of integer and fractional bits to perform ln, @@ -188,7 +197,7 @@ pub mod pallet { #[pallet::constant] type BaseDeposit: Get>; - /// The origin for most permissionless and priviledged operations. + /// The origin for most permissionless and privileged operations. type DefaultOrigin: EnsureOrigin; /// The dedicated origin for creating new bonded currency pools /// (typically permissionless). @@ -226,6 +235,7 @@ pub mod pallet { } #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); #[pallet::hooks] @@ -278,6 +288,12 @@ pub mod pallet { id: T::PoolId, manager: Option, }, + /// The asset managing team of a pool has been reset. + TeamChanged { + id: T::PoolId, + admin: T::AccountId, + freezer: T::AccountId, + }, } #[pallet::error] @@ -326,7 +342,6 @@ pub mod pallet { Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign + TryFrom + TryInto, CollateralBalanceOf: Into + TryFrom, FungiblesBalanceOf: Into + TryFrom, - // TODO: make large integer type configurable { /// Creates a new bonded token pool. The pool will be created with the /// given curve, collateral currency, and bonded currencies. The pool @@ -337,17 +352,26 @@ pub mod pallet { /// - `curve`: The curve parameters for the pool. /// - `collateral_id`: The ID of the collateral currency. /// - `currencies`: A bounded vector of token metadata for the bonded - /// currencies. - /// - `denomination`: The denomination for the bonded currencies. - /// - `transferable`: A boolean indicating if the bonded currencies are - /// transferable. + /// currencies. Note that no two currencies may use the same name or + /// symbol. + /// - `currencies_settings`: Options and settings shared by all bonded + /// currencies. These cannot be changed after the pool is created. + /// - `denomination`: The denomination for the bonded currencies. + /// - `transferable`: A boolean indicating if the bonded currencies + /// are transferable. + /// - `allow_reset_team`: Whether asset management team changes are + /// allowed for this pool. + /// - `min_operation_balance`: The minimum amount that can be + /// minted/burnt. /// /// # Returns /// - `DispatchResult`: The result of the dispatch. /// /// # Errors - /// - `Error::::InvalidInput`: If the denomination is greater than - /// the maximum allowed or if the curve input is invalid. + /// - `Error::::InvalidInput`: If either + /// - the denomination is greater than the maximum allowed + /// - the curve input is invalid + /// - two currencies use the same name or symbol /// - `Error::::Internal`: If the conversion to `BoundedVec` fails. /// - Other errors depending on the types in the config. #[pallet::call_index(0)] @@ -364,12 +388,17 @@ pub mod pallet { curve: CurveInput>, collateral_id: CollateralAssetIdOf, currencies: BoundedVec, T::MaxCurrenciesPerPool>, - denomination: u8, - transferable: bool, - min_operation_balance: u128, + currencies_settings: BondedCurrenciesSettingsOf, ) -> DispatchResult { let who = T::PoolCreateOrigin::ensure_origin(origin)?; + let BondedCurrenciesSettings { + denomination, + transferable, + allow_reset_team, + min_operation_balance, + } = currencies_settings; + ensure!(denomination <= T::MaxDenomination::get(), Error::::InvalidInput); let checked_curve = curve.try_into().map_err(|_| Error::::InvalidInput)?; @@ -407,6 +436,10 @@ pub mod pallet { // currency to it. This should also verify that the currency actually exists. T::Collaterals::touch(collateral_id.clone(), pool_account, &who)?; + // Enforce unique names and symbols by recording seen values in a set + let mut names_seen = BTreeSet::>::new(); + let mut symbols_seen = BTreeSet::>::new(); + currencies.into_iter().zip(currency_ids.iter()).try_for_each( |(token_metadata, asset_id)| -> DispatchResult { let TokenMeta { @@ -415,6 +448,11 @@ pub mod pallet { symbol, } = token_metadata; + // insert() returns true if the set did not contain the inserted value + let name_ok = name.is_empty() || names_seen.insert(name.clone()); + let symbol_ok = symbol.is_empty() || symbols_seen.insert(symbol.clone()); + ensure!(name_ok && symbol_ok, Error::::InvalidInput); + T::Fungibles::create(asset_id.clone(), pool_account.to_owned(), false, min_balance)?; // set metadata for new asset class @@ -438,6 +476,7 @@ pub mod pallet { collateral_id, currency_ids, transferable, + allow_reset_team, denomination, min_operation_balance, deposit_amount, @@ -449,28 +488,28 @@ pub mod pallet { Ok(()) } - /// Changes the managing team of a bonded currency which is issued by - /// this pool. The new team will be set to the provided team. The - /// currency index is used to select the currency that the team will - /// manage. The origin account must be a manager of the pool. + /// Changes the managing team of all bonded currencies issued by this + /// pool, setting it to the provided `team`. The origin account must be + /// a manager of the pool. /// /// # Parameters /// - `origin`: The origin of the call, requiring the caller to be a /// manager of the pool. /// - `pool_id`: The identifier of the pool. /// - `team`: The new managing team. - /// - `currency_idx`: The index of the currency in the bonded currencies - /// vector. + /// - `currency_count`: The number of bonded currencies vector linked to + /// the pool. Required for weight estimations. /// /// # Returns /// - `DispatchResult`: The result of the dispatch. /// /// # Errors /// - `Error::::PoolUnknown`: If the pool does not exist. - /// - `Error::::NoPermission`: If the caller is not a manager of the - /// pool. - /// - `Error::::IndexOutOfBounds`: If the currency index is out of - /// bounds. + /// - `Error::::NoPermission`: If this pool does not allow changing + /// the asset management team, or if the caller is not a manager of + /// the pool. + /// - `Error::::CurrencyCount`: If the actual number of currencies in + /// the pool is larger than `currency_count`. /// - Other errors depending on the types in the config. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::reset_team())] @@ -478,31 +517,44 @@ pub mod pallet { origin: OriginFor, pool_id: T::PoolId, team: PoolManagingTeam>, - currency_idx: u32, + currency_count: u32, ) -> DispatchResult { let who = T::DefaultOrigin::ensure_origin(origin)?; let pool_details = Pools::::get(&pool_id).ok_or(Error::::PoolUnknown)?; - ensure!(pool_details.is_manager(&who), Error::::NoPermission); - ensure!(pool_details.state.is_live(), Error::::PoolNotLive); + let number_of_currencies = Self::get_currencies_number(&pool_details); + ensure!(number_of_currencies <= currency_count, Error::::CurrencyCount); - let asset_id = pool_details - .bonded_currencies - .get(currency_idx.saturated_into::()) - .ok_or(Error::::IndexOutOfBounds)?; + let BondedCurrenciesSettings { allow_reset_team, .. } = pool_details.currencies_settings; + + ensure!( + allow_reset_team && pool_details.is_manager(&who), + Error::::NoPermission + ); + ensure!(pool_details.state.is_live(), Error::::PoolNotLive); - let pool_id_account = pool_id.into(); + let pool_id_account = pool_id.clone().into(); let PoolManagingTeam { freezer, admin } = team; - T::Fungibles::reset_team( - asset_id.to_owned(), - pool_id_account.clone(), + pool_details.bonded_currencies.into_iter().try_for_each(|asset_id| { + T::Fungibles::reset_team( + asset_id, + pool_id_account.clone(), + admin.clone(), + pool_id_account.clone(), + freezer.clone(), + ) + })?; + + Self::deposit_event(Event::TeamChanged { + id: pool_id, admin, - pool_id_account, freezer, - ) + }); + + Ok(()) } /// Resets the manager of a pool. The new manager will be set to the @@ -525,6 +577,8 @@ pub mod pallet { /// - `Error::::PoolUnknown`: If the pool does not exist. /// - `Error::::NoPermission`: If the caller is not a manager of the /// pool. + /// - `Error::::PoolNotLive`: If the pool is not in active or locked + /// state. #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::reset_manager())] pub fn reset_manager( @@ -535,6 +589,7 @@ pub mod pallet { let who = T::DefaultOrigin::ensure_origin(origin)?; Pools::::try_mutate(&pool_id, |maybe_entry| -> DispatchResult { let entry = maybe_entry.as_mut().ok_or(Error::::PoolUnknown)?; + ensure!(entry.state.is_live(), Error::::PoolNotLive); ensure!(entry.is_manager(&who), Error::::NoPermission); entry.manager = new_manager.clone(); @@ -558,7 +613,8 @@ pub mod pallet { /// - `origin`: The origin of the call, requiring the caller to be a /// manager of the pool. /// - `pool_id`: The identifier of the pool to be locked. - /// - `lock`: The locks to be applied to the pool. + /// - `lock`: The locks to be applied to the pool. At least one lock + /// flag must be set to `false` for a valid lock. /// /// # Returns /// - `DispatchResult`: The result of the dispatch. @@ -569,11 +625,16 @@ pub mod pallet { /// pool. /// - `Error::::PoolNotLive`: If the pool is not in a live (locked or /// active) state. + /// - `Error::::InvalidInput`: If all lock flags are `true` (all + /// operations enabled), which would be equivalent to an unlocked + /// (active) pool state. #[pallet::call_index(3)] #[pallet::weight(T::WeightInfo::set_lock())] pub fn set_lock(origin: OriginFor, pool_id: T::PoolId, lock: Locks) -> DispatchResult { let who = T::DefaultOrigin::ensure_origin(origin)?; + ensure!(lock.any_lock_set(), Error::::InvalidInput); + Pools::::try_mutate(&pool_id, |pool| -> DispatchResult { let entry = pool.as_mut().ok_or(Error::::PoolUnknown)?; ensure!(entry.state.is_live(), Error::::PoolNotLive); @@ -683,8 +744,15 @@ pub mod pallet { let number_of_currencies = Self::get_currencies_number(&pool_details); ensure!(number_of_currencies <= currency_count, Error::::CurrencyCount); + let BondedCurrenciesSettings { + min_operation_balance, + denomination, + transferable, + .. + } = pool_details.currencies_settings; + ensure!( - amount_to_mint >= pool_details.min_operation_balance.saturated_into(), + amount_to_mint >= min_operation_balance.saturated_into(), TokenError::BelowMinimum ); @@ -704,16 +772,13 @@ pub mod pallet { let (active_pre, passive) = Self::calculate_normalized_passive_issuance( &bonded_currencies, - pool_details.denomination, + denomination, currency_idx, round_kind, )?; - let normalized_amount_to_mint = balance_to_fixed( - amount_to_mint.saturated_into::(), - pool_details.denomination, - round_kind, - )?; + let normalized_amount_to_mint = + balance_to_fixed(amount_to_mint.saturated_into::(), denomination, round_kind)?; let active_post = active_pre .checked_add(normalized_amount_to_mint) @@ -744,7 +809,7 @@ pub mod pallet { T::Fungibles::mint_into(target_currency_id.clone(), &beneficiary, amount_to_mint)?; - if !pool_details.transferable { + if !transferable { T::Fungibles::freeze(target_currency_id, &beneficiary).map_err(|freeze_error| { log::info!(target: LOG_TARGET, "Failed to freeze account: {:?}", freeze_error); freeze_error.into() @@ -812,8 +877,15 @@ pub mod pallet { let pool_details = Pools::::get(&pool_id).ok_or(Error::::PoolUnknown)?; + let BondedCurrenciesSettings { + min_operation_balance, + denomination, + transferable, + .. + } = pool_details.currencies_settings; + ensure!( - amount_to_burn >= pool_details.min_operation_balance.saturated_into(), + amount_to_burn >= min_operation_balance.saturated_into(), TokenError::BelowMinimum ); @@ -834,12 +906,12 @@ pub mod pallet { let (high, passive) = Self::calculate_normalized_passive_issuance( &bonded_currencies, - pool_details.denomination, + denomination, currency_idx, round_kind, )?; - let normalized_amount_to_burn = balance_to_fixed(amount_to_burn, pool_details.denomination, round_kind)?; + let normalized_amount_to_burn = balance_to_fixed(amount_to_burn, denomination, round_kind)?; let low = high .checked_sub(normalized_amount_to_burn) @@ -885,7 +957,7 @@ pub mod pallet { let account_exists = T::Fungibles::total_balance(target_currency_id.clone(), &who) > Zero::zero(); - if !pool_details.transferable && account_exists { + if !transferable && account_exists { // Restore locks. T::Fungibles::freeze(target_currency_id, &who).map_err(|freeze_error| { log::info!(target: LOG_TARGET, "Failed to freeze account: {:?}", freeze_error); @@ -1078,11 +1150,6 @@ pub mod pallet { .checked_add(burnt) .ok_or(ArithmeticError::Overflow)?; - defensive_assert!( - sum_of_issuances >= burnt, - "burnt amount exceeds the total supply of all bonded currencies" - ); - let amount: CollateralBalanceOf = burnt .checked_mul(total_collateral_issuance.into()) // As long as the balance type is half the size of a U256, this won't overflow. @@ -1117,16 +1184,16 @@ pub mod pallet { Error::::Internal })?; - if amount.is_zero() - || T::Collaterals::can_deposit(pool_details.collateral.clone(), &who, amount, Provenance::Extant) - .into_result() - .is_err() - { + let deposit_consequence = + T::Collaterals::can_deposit(pool_details.collateral.clone(), &who, amount, Provenance::Extant); + if amount.is_zero() || deposit_consequence == DepositConsequence::BelowMinimum { // Funds are burnt but the collateral received is not sufficient to be deposited // to the account. This is tolerated as otherwise we could have edge cases where // it's impossible to refund at least some accounts. return Ok(Some(T::WeightInfo::refund_account(currency_count.to_owned())).into()); } + // Return error if not Success + deposit_consequence.into_result()?; let transferred = T::Collaterals::transfer( pool_details.collateral, @@ -1418,7 +1485,6 @@ pub mod pallet { ensure!(pool_details.state.is_live(), Error::::PoolNotLive); if let Some(caller) = maybe_check_manager { - // TODO: should the owner be authorized as well? ensure!(pool_details.is_manager(caller), Error::::NoPermission); } @@ -1491,7 +1557,6 @@ pub mod pallet { ); if let Some(caller) = maybe_check_manager { - // TODO: should this be permissionless if the pool is in refunding state? ensure!( pool_details.is_owner(caller) || pool_details.is_manager(caller), Error::::NoPermission diff --git a/pallets/pallet-bonded-coins/src/migrations/mod.rs b/pallets/pallet-bonded-coins/src/migrations/mod.rs new file mode 100644 index 000000000..a3a6d96c3 --- /dev/null +++ b/pallets/pallet-bonded-coins/src/migrations/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/pallets/pallet-bonded-coins/src/migrations/v1.rs b/pallets/pallet-bonded-coins/src/migrations/v1.rs new file mode 100644 index 000000000..4f56bf754 --- /dev/null +++ b/pallets/pallet-bonded-coins/src/migrations/v1.rs @@ -0,0 +1,177 @@ +use frame_support::{ + pallet_prelude::*, + traits::{Get, OnRuntimeUpgrade}, +}; +use sp_runtime::traits::Saturating; + +use crate::{ + curves::Curve, + types::{Locks, PoolStatus}, + BondedCurrenciesSettingsOf, BoundedCurrencyVec, CollateralAssetIdOf, Config, CurveParameterTypeOf, + DepositBalanceOf, FungiblesBalanceOf, +}; + +#[cfg(feature = "try-runtime")] +const LOG_TARGET: &str = "migration::pallet-bonded-coins"; + +/// Collection of storage item formats from the previous storage version. +/// +/// Required so we can read values in the v0 storage format during the +/// migration. +mod v0 { + use super::*; + use frame_support::{storage_alias, Twox64Concat}; + use parity_scale_codec::{Decode, Encode}; + use scale_info::TypeInfo; + + // V0 pool details + #[derive(Clone, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen, Debug)] + pub struct PoolDetails { + /// The owner of the pool. + pub owner: AccountId, + /// The manager of the pool. If a manager is set, the pool is + /// permissioned. + pub manager: Option, + /// The curve of the pool. + pub curve: ParametrizedCurve, + /// The collateral currency of the pool. + pub collateral: BaseCurrencyId, + /// The bonded currencies of the pool. + pub bonded_currencies: Currencies, + /// The status of the pool. + pub state: PoolStatus, + /// Whether the pool is transferable or not. + pub transferable: bool, + /// The denomination of the pool. + pub denomination: u8, + /// The minimum amount that can be minted/burnt. + pub min_operation_balance: FungiblesBalance, + /// The deposit to be returned upon destruction of this pool. + pub deposit: DepositBalance, + } + + pub type PoolDetailsOf = PoolDetails< + ::AccountId, + Curve>, + BoundedCurrencyVec, + CollateralAssetIdOf, + DepositBalanceOf, + FungiblesBalanceOf, + >; + + /// V0 type for [`crate::Pools`]. + #[storage_alias] + pub type Pools = + StorageMap, Twox64Concat, ::PoolId, PoolDetailsOf, OptionQuery>; +} + +fn v0_to_v1(old_value: v0::PoolDetailsOf) -> crate::PoolDetailsOf { + let v0::PoolDetailsOf:: { + owner, + curve, + manager, + collateral, + bonded_currencies, + state, + transferable, + denomination, + min_operation_balance, + deposit, + } = old_value; + + crate::PoolDetailsOf:: { + owner, + curve, + manager, + collateral, + bonded_currencies, + state, + deposit, + currencies_settings: BondedCurrenciesSettingsOf:: { + denomination, + min_operation_balance, + transferable, + allow_reset_team: true, + }, + } +} + +pub struct InnerMigrateV0ToV1(core::marker::PhantomData); + +impl OnRuntimeUpgrade for InnerMigrateV0ToV1 +where + T::PoolId: sp_std::fmt::Debug, +{ + /// Return a vector of existing [`crate::Pools`] values so we can check that + /// they were correctly set in `InnerMigrateV0ToV1::post_upgrade`. + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + // Access the old value using the `storage_alias` type + let old_value: sp_std::vec::Vec<(T::PoolId, v0::PoolDetailsOf)> = v0::Pools::::iter().collect(); + // Return it as an encoded `Vec` + Ok(old_value.encode()) + } + + /// Migrate the storage from V0 to V1. + fn on_runtime_upgrade() -> frame_support::weights::Weight { + let mut translated = 0u64; + // Read values in-place + crate::Pools::::translate_values::, _>(|old_value| { + translated.saturating_inc(); + Some(v0_to_v1::(old_value)) + }); + + // One read for taking the old value, and one write for setting the new value + T::DbWeight::get().reads_writes(translated, translated) + } + + /// Verifies the storage was migrated correctly. + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: sp_std::vec::Vec) -> Result<(), sp_runtime::TryRuntimeError> { + use sp_runtime::traits::SaturatedConversion; + + let old_values = sp_std::vec::Vec::<(T::PoolId, v0::PoolDetailsOf)>::decode(&mut &state[..]) + .map_err(|_| sp_runtime::TryRuntimeError::Other("Failed to decode old value from storage"))?; + + let prev_count: u32 = old_values.len().saturated_into(); + let post_count: u32 = crate::Pools::::iter().count().saturated_into(); + + ensure!( + prev_count == post_count, + "the pool count before and after the migration should be the same" + ); + + log::info!(target: LOG_TARGET, "Migrated {} pool entries", post_count); + + old_values.into_iter().try_for_each(|(pool_id, old_value)| { + let expected_new_value = v0_to_v1::(old_value); + let actual_new_value = crate::Pools::::get(&pool_id); + + ensure!(actual_new_value.is_some(), { + log::error!(target: LOG_TARGET, "Expected pool with id {:?} but found none", &pool_id); + sp_runtime::TryRuntimeError::Other("Pool not migrated") + }); + ensure!(actual_new_value == Some(expected_new_value), { + log::error!(target: LOG_TARGET, "Pool with id {:?} contains unexpected data", &pool_id); + sp_runtime::TryRuntimeError::Other("Incorrect Pool Data") + }); + + ensure!(actual_new_value.unwrap().currencies_settings.allow_reset_team, { + log::error!(target: LOG_TARGET, "Pool with id {:?} has allow_reset_team = false", &pool_id); + sp_runtime::TryRuntimeError::Other( + "all migrated pools should have the allow_reset_team flag set to true", + ) + }); + + Ok(()) + }) + } +} + +pub type MigrateV0ToV1 = frame_support::migrations::VersionedMigration< + 0, // The migration will only execute when the on-chain storage version is 0 + 1, // The on-chain storage version will be set to 1 after the migration is complete + InnerMigrateV0ToV1, + crate::pallet::Pallet, + ::DbWeight, +>; diff --git a/pallets/pallet-bonded-coins/src/mock.rs b/pallets/pallet-bonded-coins/src/mock.rs index 814334c66..1a77a47a8 100644 --- a/pallets/pallet-bonded-coins/src/mock.rs +++ b/pallets/pallet-bonded-coins/src/mock.rs @@ -58,6 +58,7 @@ pub mod runtime { weights::constants::RocksDbWeight, }; use frame_system::{EnsureRoot, EnsureSigned}; + use pallet_assets::FrozenBalance; use sp_core::U256; use sp_runtime::{ traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, @@ -68,8 +69,8 @@ pub mod runtime { use crate::{ self as pallet_bonded_coins, traits::NextAssetIds, - types::{Locks, PoolStatus}, - Config, DepositBalanceOf, FungiblesAssetIdOf, PoolDetailsOf, + types::{BondedCurrenciesSettings, Locks, PoolStatus}, + AccountIdOf, Config, DepositBalanceOf, FungiblesAssetIdOf, FungiblesBalanceOf, PoolDetailsOf, }; pub type Hash = sp_core::H256; @@ -117,13 +118,16 @@ pub mod runtime { PoolDetailsOf:: { curve, manager, - transferable, bonded_currencies, state, collateral, - denomination: DEFAULT_BONDED_DENOMINATION, + currencies_settings: BondedCurrenciesSettings { + allow_reset_team: true, + transferable, + min_operation_balance, + denomination: DEFAULT_BONDED_DENOMINATION, + }, owner, - min_operation_balance, deposit: BondingPallet::calculate_pool_deposit(currencies.len()), } } @@ -190,6 +194,28 @@ pub mod runtime { } } + /// Store freezes for the assets pallet. + #[storage_alias] + pub type Freezes = StorageDoubleMap< + Assets, + Blake2_128Concat, + FungiblesAssetIdOf, + Blake2_128Concat, + AccountIdOf, + FungiblesBalanceOf, + OptionQuery, + >; + + pub struct FreezesHook; + + impl FrozenBalance for FreezesHook { + fn died(_asset: AssetId, _who: &AccountId) {} + + fn frozen_balance(asset: AssetId, who: &AccountId) -> Option { + Freezes::::get(asset, who) + } + } + frame_support::construct_runtime!( pub enum Test { @@ -279,7 +305,7 @@ pub mod runtime { type Currency = Balances; type Extra = (); type ForceOrigin = EnsureRoot; - type Freezer = (); + type Freezer = FreezesHook; type MetadataDepositBase = ConstU128<0>; type MetadataDepositPerByte = ConstU128<0>; type RemoveItemsLimit = ConstU32<5>; @@ -355,6 +381,7 @@ pub mod runtime { // pool_id, PoolDetails pools: Vec<(AccountId, PoolDetailsOf)>, collaterals: Vec, + freezes: Vec<(AssetId, AccountId, Balance)>, } impl ExtBuilder { @@ -378,6 +405,11 @@ pub mod runtime { self } + pub(crate) fn with_freezes(mut self, freezes: Vec<(AssetId, AccountId, Balance)>) -> Self { + self.freezes = freezes; + self + } + pub(crate) fn build(self) -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); @@ -417,7 +449,7 @@ pub mod runtime { pool_details .bonded_currencies .iter() - .map(|id| (*id, vec![], vec![], pool_details.denomination)) + .map(|id| (*id, vec![], vec![], pool_details.currencies_settings.denomination)) .collect::, Vec, u8)>>() }) .chain( @@ -448,6 +480,10 @@ pub mod runtime { }); NextAssetId::::set(next_asset_id); + + self.freezes.iter().for_each(|(asset_id, account, amount)| { + Freezes::::set(asset_id, account, Some(*amount)); + }); }); ext diff --git a/pallets/pallet-bonded-coins/src/tests/transactions/burn_into.rs b/pallets/pallet-bonded-coins/src/tests/transactions/burn_into.rs index 13d965962..86e644c3b 100644 --- a/pallets/pallet-bonded-coins/src/tests/transactions/burn_into.rs +++ b/pallets/pallet-bonded-coins/src/tests/transactions/burn_into.rs @@ -27,7 +27,7 @@ use sp_runtime::{assert_eq_error_rate, bounded_vec, traits::Scale, TokenError}; use crate::{ mock::{runtime::*, *}, - types::{Locks, PoolStatus}, + types::{BondedCurrenciesSettings, Locks, PoolStatus}, AccountIdOf, Error, PoolDetailsOf, }; @@ -55,13 +55,16 @@ fn burn_first_coin() { PoolDetailsOf:: { curve: get_linear_bonding_curve(), manager: None, - transferable: true, bonded_currencies: bounded_vec![DEFAULT_BONDED_CURRENCY_ID], state: PoolStatus::Active, collateral: DEFAULT_COLLATERAL_CURRENCY_ID, - denomination: 0, + currencies_settings: BondedCurrenciesSettings { + transferable: true, + allow_reset_team: true, + denomination: 0, + min_operation_balance: 1, + }, owner: ACCOUNT_99, - min_operation_balance: 1, deposit: BondingPallet::calculate_pool_deposit(1), }, )]) 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 8e6b7734e..a213a012a 100644 --- a/pallets/pallet-bonded-coins/src/tests/transactions/create_pool.rs +++ b/pallets/pallet-bonded-coins/src/tests/transactions/create_pool.rs @@ -28,8 +28,8 @@ use sp_std::ops::Sub; use crate::{ mock::{runtime::*, *}, - types::{Locks, PoolStatus}, - AccountIdOf, Event as BondingPalletEvents, Pools, TokenMetaOf, + types::{BondedCurrenciesSettings, Locks, PoolStatus}, + AccountIdOf, Error, Event as BondingPalletEvents, Pools, TokenMetaOf, }; #[test] @@ -55,9 +55,12 @@ fn single_currency() { curve, DEFAULT_COLLATERAL_CURRENCY_ID, bounded_vec![bonded_token], - DEFAULT_BONDED_DENOMINATION, - true, - 1, + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + }, )); let pool_id: AccountIdOf = calculate_pool_id(&[new_asset_id]); @@ -66,7 +69,7 @@ fn single_currency() { assert!(details.is_owner(&ACCOUNT_00)); assert!(details.is_manager(&ACCOUNT_00)); - assert!(details.transferable); + assert!(details.currencies_settings.transferable); assert_eq!( details.state, PoolStatus::Locked(Locks { @@ -74,7 +77,7 @@ fn single_currency() { allow_burn: false, }) ); - assert_eq!(details.denomination, DEFAULT_BONDED_DENOMINATION); + assert_eq!(details.currencies_settings.denomination, DEFAULT_BONDED_DENOMINATION); assert_eq!(details.collateral, DEFAULT_COLLATERAL_CURRENCY_ID); assert_eq!(details.bonded_currencies, vec![new_asset_id]); @@ -104,6 +107,71 @@ fn single_currency() { #[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_and_execute_with_sanity_tests(|| { + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_tokens = bounded_vec![ + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"BTC".to_vec()), + min_balance: 1, + }, + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Ether".to_vec()), + symbol: BoundedVec::truncate_from(b"ETH".to_vec()), + min_balance: 1, + }, + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Dogecoin".to_vec()), + symbol: BoundedVec::truncate_from(b"DOGE".to_vec()), + min_balance: 1, + } + ]; + + let next_asset_id = NextAssetId::::get(); + + assert_ok!(BondingPallet::create_pool( + origin, + curve, + DEFAULT_COLLATERAL_CURRENCY_ID, + bonded_tokens, + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } + )); + + assert_eq!(NextAssetId::::get(), next_asset_id + 3); + + let new_assets = Vec::from_iter(next_asset_id..next_asset_id + 3); + let pool_id: AccountIdOf = 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!(Assets::asset_exists(new_asset_id)); + assert_eq!(Assets::owner(new_asset_id), Some(pool_id.clone())); + } + }); +} + +#[test] +fn multi_currency_with_empty_metadata() { let initial_balance = ONE_HUNDRED_KILT; ExtBuilder::default() .with_native_balances(vec![(ACCOUNT_00, initial_balance)]) @@ -113,13 +181,12 @@ fn multi_currency() { 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()), + name: BoundedVec::new(), + symbol: BoundedVec::new(), min_balance: 1, }; let bonded_tokens = bounded_vec![bonded_token; 3]; - let next_asset_id = NextAssetId::::get(); assert_ok!(BondingPallet::create_pool( @@ -127,9 +194,12 @@ fn multi_currency() { curve, DEFAULT_COLLATERAL_CURRENCY_ID, bonded_tokens, - DEFAULT_BONDED_DENOMINATION, - true, - 1 + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } )); assert_eq!(NextAssetId::::get(), next_asset_id + 3); @@ -177,9 +247,12 @@ fn can_create_identical_pools() { curve.clone(), DEFAULT_COLLATERAL_CURRENCY_ID, bounded_vec![bonded_token.clone()], - DEFAULT_BONDED_DENOMINATION, - true, - 1 + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } )); assert_ok!(BondingPallet::create_pool( @@ -187,9 +260,12 @@ fn can_create_identical_pools() { curve, DEFAULT_COLLATERAL_CURRENCY_ID, bounded_vec![bonded_token], - DEFAULT_BONDED_DENOMINATION, - true, - 1 + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } )); assert_eq!(NextAssetId::::get(), next_asset_id + 2); @@ -207,6 +283,98 @@ fn can_create_identical_pools() { }); } +#[test] +fn cannot_reuse_names() { + let initial_balance = ONE_HUNDRED_KILT; + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_00, initial_balance)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build_and_execute_with_sanity_tests(|| { + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_tokens = bounded_vec![ + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"BTC".to_vec()), + min_balance: 1, + }, + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Ether".to_vec()), + symbol: BoundedVec::truncate_from(b"ETH".to_vec()), + min_balance: 1, + }, + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"DOGE".to_vec()), + min_balance: 1, + } + ]; + + assert_err!( + BondingPallet::create_pool( + origin, + curve, + DEFAULT_COLLATERAL_CURRENCY_ID, + bonded_tokens, + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } + ), + Error::::InvalidInput + ); + }); +} + +#[test] +fn cannot_reuse_symbols() { + let initial_balance = ONE_HUNDRED_KILT; + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_00, initial_balance)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build_and_execute_with_sanity_tests(|| { + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + let curve = get_linear_bonding_curve_input(); + + let bonded_tokens = bounded_vec![ + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Bitcoin".to_vec()), + symbol: BoundedVec::truncate_from(b"BTC".to_vec()), + min_balance: 1, + }, + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Ether".to_vec()), + symbol: BoundedVec::truncate_from(b"ETH".to_vec()), + min_balance: 1, + }, + TokenMetaOf:: { + name: BoundedVec::truncate_from(b"Dogecoin".to_vec()), + symbol: BoundedVec::truncate_from(b"BTC".to_vec()), + min_balance: 1, + } + ]; + + assert_err!( + BondingPallet::create_pool( + origin, + curve, + DEFAULT_COLLATERAL_CURRENCY_ID, + bonded_tokens, + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } + ), + Error::::InvalidInput + ); + }); +} + #[test] fn fails_if_collateral_not_exists() { ExtBuilder::default() @@ -227,9 +395,12 @@ fn fails_if_collateral_not_exists() { curve, 100, bounded_vec![bonded_token], - DEFAULT_BONDED_DENOMINATION, - true, - 1 + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } ), AssetsPalletErrors::::Unknown ); @@ -259,9 +430,12 @@ fn cannot_create_circular_pool() { // try specifying the id of the currency to be created as collateral next_asset_id, bounded_vec![bonded_token], - DEFAULT_BONDED_DENOMINATION, - true, - 1 + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } ), AssetsPalletErrors::::Unknown ); @@ -292,9 +466,12 @@ fn handles_asset_id_overflow() { curve, DEFAULT_COLLATERAL_CURRENCY_ID, bounded_vec![bonded_token; 2], - DEFAULT_BONDED_DENOMINATION, - true, - 1 + BondedCurrenciesSettings { + denomination: DEFAULT_BONDED_DENOMINATION, + allow_reset_team: true, + transferable: true, + min_operation_balance: 1 + } ), ArithmeticError::Overflow ); diff --git a/pallets/pallet-bonded-coins/src/tests/transactions/mint_into.rs b/pallets/pallet-bonded-coins/src/tests/transactions/mint_into.rs index 8440b670d..61e283e9d 100644 --- a/pallets/pallet-bonded-coins/src/tests/transactions/mint_into.rs +++ b/pallets/pallet-bonded-coins/src/tests/transactions/mint_into.rs @@ -759,10 +759,6 @@ fn mint_without_collateral() { #[test] fn mint_more_than_fixed_can_represent() { - // denomination is 10 - // capacity of I75F53 is 1.8+e22 - // -> we need to get beyond 1.8+e32 - // check that we can still burn afterwards let pool_id: AccountIdOf = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]); let curve = Curve::Polynomial(PolynomialParameters { @@ -771,7 +767,11 @@ fn mint_more_than_fixed_can_represent() { o: Float::from_num(0.1), }); - let amount_to_mint = 10u128.pow(20); + // capacity of I75F53 is (2^74)-1 + // denomination is 10 + // -> minting 2^74 * 10^10 coins would just barely overflow + // we do it in two tranches, first should work, second should fail + let amount_to_mint = 2u128.pow(74) * 10u128.pow(10) / 2; ExtBuilder::default() .with_native_balances(vec![(ACCOUNT_00, ONE_HUNDRED_KILT)]) @@ -798,11 +798,18 @@ fn mint_more_than_fixed_can_represent() { .build_and_execute_with_sanity_tests(|| { let origin: OriginFor = RawOrigin::Signed(ACCOUNT_00).into(); - // repeatedly mint until we hit balance that cannot be represented - let mut result = Ok(().into()); - let mut mints = 0; - while result.is_ok() { - result = BondingPallet::mint_into( + assert_ok!(BondingPallet::mint_into( + origin.clone(), + pool_id.clone(), + 0, + ACCOUNT_00, + amount_to_mint, + u128::MAX, + 1, + )); + + assert_err!( + BondingPallet::mint_into( origin.clone(), pool_id.clone(), 0, @@ -810,16 +817,13 @@ fn mint_more_than_fixed_can_represent() { amount_to_mint, u128::MAX, 1, - ); - mints += 1; - } - - assert!(mints > 2); - assert_err!(result, ArithmeticError::Overflow); + ), + ArithmeticError::Overflow + ); assert_eq!( Assets::total_balance(DEFAULT_BONDED_CURRENCY_ID, &ACCOUNT_00), - amount_to_mint * (mints - 1) + amount_to_mint ); // Make sure the pool is not stuck diff --git a/pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs b/pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs index b7a21ae86..01fc5e7b2 100644 --- a/pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs +++ b/pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs @@ -17,9 +17,9 @@ // If you feel like getting in touch with us, you can do so at use frame_support::{ assert_err, assert_ok, - traits::fungibles::{Create, Inspect, Mutate}, + traits::fungibles::{roles::Inspect as InspectRole, Create, Inspect, Mutate}, }; -use frame_system::{pallet_prelude::OriginFor, RawOrigin}; +use frame_system::{pallet_prelude::OriginFor, ConsumerLimits, RawOrigin}; use sp_runtime::TokenError; use crate::{ @@ -266,6 +266,112 @@ fn refund_below_min_balance() { }); } +#[test] +fn refund_account_fails_when_account_blocked() { + let pool_details = generate_pool_details( + vec![DEFAULT_BONDED_CURRENCY_ID], + get_linear_bonding_curve(), + false, + Some(PoolStatus::Refunding), + Some(ACCOUNT_00), + None, + None, + None, + ); + let pool_id: AccountIdOf = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]); + + ExtBuilder::default() + .with_native_balances(vec![(ACCOUNT_01, ONE_HUNDRED_KILT)]) + .with_pools(vec![(pool_id.clone(), pool_details)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .with_bonded_balance(vec![ + (DEFAULT_COLLATERAL_CURRENCY_ID, pool_id.clone(), ONE_HUNDRED_KILT), + (DEFAULT_COLLATERAL_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT), + (DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT), + ]) + .build_and_execute_with_sanity_tests(|| { + let origin: OriginFor = RawOrigin::Signed(ACCOUNT_01).into(); + + Assets::block( + RawOrigin::Signed(Assets::owner(DEFAULT_COLLATERAL_CURRENCY_ID).unwrap()).into(), + DEFAULT_COLLATERAL_CURRENCY_ID, + ACCOUNT_01, + ) + .expect("Failed to block account for test"); + + // Ensure the refund_account call fails due to failing can_deposit check + assert_err!( + BondingPallet::refund_account(origin, pool_id.clone(), ACCOUNT_01, 0, 1), + TokenError::Blocked + ); + }); +} + +#[test] +fn refund_account_fails_if_account_cannot_be_created() { + let pool_details = generate_pool_details( + vec![DEFAULT_BONDED_CURRENCY_ID], + get_linear_bonding_curve(), + false, + Some(PoolStatus::Refunding), + Some(ACCOUNT_00), + None, + None, + None, + ); + let pool_id: AccountIdOf = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]); + + // get MaxConsumers value + let max_consumers: usize = ::MaxConsumers::max_consumers() + .try_into() + .expect(""); + // Collateral must be non-sufficient for these tests to work, so we'll create a + // new collateral asset in the test + let collateral_id = DEFAULT_BONDED_CURRENCY_ID + 1; + + ExtBuilder::default() + .with_native_balances(vec![ + (ACCOUNT_01, ONE_HUNDRED_KILT), + (pool_id.clone(), ONE_HUNDRED_KILT), + ]) + .with_pools(vec![(pool_id.clone(), pool_details)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .with_bonded_balance(vec![ + (DEFAULT_COLLATERAL_CURRENCY_ID, pool_id.clone(), ONE_HUNDRED_KILT), + (DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT), + ]) + // Freeze some funds for ACCOUNT_01 so refund can only be partial + .with_freezes(vec![(DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_01, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + // switch pool's collateral to a non-sufficient one + assert_ok!(>::create(collateral_id, ACCOUNT_00, false, 1)); + Pools::::mutate(&pool_id, |details| details.as_mut().unwrap().collateral = collateral_id); + // make sure pool account holds collateral - this should work because it also + // holds the (sufficient) original collateral + assert_ok!(Assets::mint_into(collateral_id, &pool_id, ONE_HUNDRED_KILT)); + // create non-sufficient assets to increase ACCOUNT_01 consumers count + // the assets pallet requires at least two references to be available for + // creating an account, but creates only one, meaning we create max_consumers - + // 2 currencies (resulting in max_consumers - 1 consumers because we created one + // previously) + for i in 1..max_consumers - 1 { + let i_u32: u32 = i.try_into().expect("Failed to convert to u32"); + let asset_id = collateral_id + i_u32; + assert_ok!(>::create(asset_id, ACCOUNT_00, false, 1)); + assert_ok!(Assets::mint_into(asset_id, &ACCOUNT_01, ONE_HUNDRED_KILT)); + } + + let origin: OriginFor = RawOrigin::Signed(ACCOUNT_01).into(); + + // Collateral is non-sufficient and thus would create additional consumer + // references on ACCOUNT_01, which is too many + assert_err!( + BondingPallet::refund_account(origin, pool_id.clone(), ACCOUNT_01, 0, 1), + TokenError::CannotCreate + ); + }); +} + #[test] fn refund_account_fails_when_pool_not_refunding() { let pool_details = generate_pool_details( diff --git a/pallets/pallet-bonded-coins/src/tests/transactions/reset_team.rs b/pallets/pallet-bonded-coins/src/tests/transactions/reset_team.rs index 027e3d652..5453a2a1d 100644 --- a/pallets/pallet-bonded-coins/src/tests/transactions/reset_team.rs +++ b/pallets/pallet-bonded-coins/src/tests/transactions/reset_team.rs @@ -20,8 +20,9 @@ use frame_system::RawOrigin; use crate::{ mock::{runtime::*, *}, + traits::ResetTeam, types::{PoolManagingTeam, PoolStatus}, - AccountIdOf, Error as BondingPalletErrors, + AccountIdOf, Error as BondingPalletErrors, Event, }; #[test] @@ -51,9 +52,19 @@ fn resets_team() { admin: ACCOUNT_00, freezer: ACCOUNT_01, }, - 0 + 1 )); + // Ensure the event is emitted + System::assert_has_event( + Event::TeamChanged { + id: pool_id.clone(), + admin: ACCOUNT_00, + freezer: ACCOUNT_01, + } + .into(), + ); + assert_eq!(Assets::admin(DEFAULT_BONDED_CURRENCY_ID), Some(ACCOUNT_00)); assert_eq!(Assets::freezer(DEFAULT_BONDED_CURRENCY_ID), Some(ACCOUNT_01)); assert_eq!(Assets::owner(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id.clone())); @@ -61,6 +72,121 @@ fn resets_team() { }) } +#[test] +fn resets_owner_if_changed() { + let pool_details = generate_pool_details( + vec![DEFAULT_BONDED_CURRENCY_ID], + get_linear_bonding_curve(), + false, + Some(PoolStatus::Active), + Some(ACCOUNT_00), + None, + None, + None, + ); + let pool_id: AccountIdOf = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]); + + ExtBuilder::default() + .with_pools(vec![(pool_id.clone(), pool_details)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build_and_execute_with_sanity_tests(|| { + Assets::reset_team( + DEFAULT_BONDED_CURRENCY_ID, + ACCOUNT_00, + pool_id.clone(), + pool_id.clone(), + pool_id.clone(), + ) + .expect("Failed to use reset_team trait"); + + assert_eq!(Assets::owner(DEFAULT_BONDED_CURRENCY_ID), Some(ACCOUNT_00)); + assert_eq!(Assets::admin(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id.clone())); + assert_eq!(Assets::issuer(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id.clone())); + assert_eq!(Assets::freezer(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id.clone())); + + let manager_origin = RawOrigin::Signed(ACCOUNT_00).into(); + + assert_ok!(BondingPallet::reset_team( + manager_origin, + pool_id.clone(), + PoolManagingTeam { + admin: pool_id.clone(), + freezer: pool_id.clone(), + }, + 1 + )); + + // Ensure the event is emitted + System::assert_has_event( + Event::TeamChanged { + id: pool_id.clone(), + admin: pool_id.clone(), + freezer: pool_id.clone(), + } + .into(), + ); + + assert_eq!(Assets::admin(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id.clone())); + assert_eq!(Assets::freezer(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id.clone())); + assert_eq!(Assets::owner(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id.clone())); + assert_eq!(Assets::issuer(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id)); + }) +} + +#[test] +fn resets_team_for_all() { + let currencies = vec![DEFAULT_BONDED_CURRENCY_ID, DEFAULT_BONDED_CURRENCY_ID + 1]; + + let pool_details = generate_pool_details( + currencies.clone(), + get_linear_bonding_curve(), + false, + Some(PoolStatus::Active), + Some(ACCOUNT_00), + None, + None, + None, + ); + let pool_id: AccountIdOf = calculate_pool_id(¤cies); + + ExtBuilder::default() + .with_pools(vec![(pool_id.clone(), pool_details)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build_and_execute_with_sanity_tests(|| { + let manager_origin = RawOrigin::Signed(ACCOUNT_00).into(); + + assert_ok!(BondingPallet::reset_team( + manager_origin, + pool_id.clone(), + PoolManagingTeam { + admin: ACCOUNT_00, + freezer: ACCOUNT_01, + }, + 2 + )); + + // Ensure the event is emitted + System::assert_has_event( + Event::TeamChanged { + id: pool_id.clone(), + admin: ACCOUNT_00, + freezer: ACCOUNT_01, + } + .into(), + ); + + assert_eq!(Assets::admin(currencies[0]), Some(ACCOUNT_00)); + assert_eq!(Assets::freezer(currencies[0]), Some(ACCOUNT_01)); + assert_eq!(Assets::owner(currencies[0]), Some(pool_id.clone())); + assert_eq!(Assets::issuer(currencies[0]), Some(pool_id.clone())); + + assert_eq!(Assets::admin(currencies[1]), Some(ACCOUNT_00)); + assert_eq!(Assets::freezer(currencies[1]), Some(ACCOUNT_01)); + assert_eq!(Assets::owner(currencies[1]), Some(pool_id.clone())); + assert_eq!(Assets::issuer(currencies[1]), Some(pool_id)); + }) +} + #[test] fn does_not_change_team_when_not_live() { let pool_details = generate_pool_details( @@ -89,7 +215,7 @@ fn does_not_change_team_when_not_live() { admin: ACCOUNT_00, freezer: ACCOUNT_00, }, - 0 + 1 ), BondingPalletErrors::::PoolNotLive ); @@ -130,7 +256,7 @@ fn only_manager_can_change_team() { admin: ACCOUNT_00, freezer: ACCOUNT_00, }, - 0 + 1 ), BondingPalletErrors::::NoPermission ); @@ -143,7 +269,47 @@ fn only_manager_can_change_team() { admin: ACCOUNT_00, freezer: ACCOUNT_00, }, - 0 + 1 + ), + BondingPalletErrors::::NoPermission + ); + + assert_eq!(Assets::admin(DEFAULT_BONDED_CURRENCY_ID), Some(pool_id)); + }) +} + +#[test] +fn fails_if_asset_team_flag_not_set() { + let curve = get_linear_bonding_curve(); + + let mut pool_details = generate_pool_details( + vec![DEFAULT_BONDED_CURRENCY_ID], + curve, + false, + Some(PoolStatus::Active), + Some(ACCOUNT_00), + None, + Some(ACCOUNT_00), + None, + ); + pool_details.currencies_settings.allow_reset_team = false; + let pool_id: AccountIdOf = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]); + ExtBuilder::default() + .with_pools(vec![(pool_id.clone(), pool_details)]) + .with_native_balances(vec![(ACCOUNT_00, ONE_HUNDRED_KILT)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .build_and_execute_with_sanity_tests(|| { + let manager_origin = RawOrigin::Signed(ACCOUNT_00).into(); + + assert_err!( + BondingPallet::reset_team( + manager_origin, + pool_id.clone(), + PoolManagingTeam { + admin: ACCOUNT_00, + freezer: ACCOUNT_00, + }, + 1 ), BondingPalletErrors::::NoPermission ); @@ -153,7 +319,7 @@ fn only_manager_can_change_team() { } #[test] -fn handles_currency_idx_out_of_bounds() { +fn handles_currency_number_incorrect() { let pool_details = generate_pool_details( vec![DEFAULT_BONDED_CURRENCY_ID], get_linear_bonding_curve(), @@ -180,9 +346,9 @@ fn handles_currency_idx_out_of_bounds() { admin: ACCOUNT_00, freezer: ACCOUNT_00, }, - 2 + 0 ), - BondingPalletErrors::::IndexOutOfBounds + BondingPalletErrors::::CurrencyCount ); }) } diff --git a/pallets/pallet-bonded-coins/src/tests/transactions/set_lock.rs b/pallets/pallet-bonded-coins/src/tests/transactions/set_lock.rs index 133bfb6e5..3271d102c 100644 --- a/pallets/pallet-bonded-coins/src/tests/transactions/set_lock.rs +++ b/pallets/pallet-bonded-coins/src/tests/transactions/set_lock.rs @@ -116,6 +116,45 @@ fn set_lock_works_when_locked() { }); } +#[test] +fn set_lock_requires_at_least_one_flag_set() { + let pool_details = generate_pool_details( + vec![DEFAULT_BONDED_CURRENCY_ID], + get_linear_bonding_curve(), + true, + Some(PoolStatus::Active), + Some(ACCOUNT_00), + Some(DEFAULT_COLLATERAL_CURRENCY_ID), + Some(ACCOUNT_00), + None, + ); + let pool_id: AccountIdOf = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]); + + ExtBuilder::default() + .with_pools(vec![(pool_id.clone(), pool_details)]) + .with_native_balances(vec![(ACCOUNT_00, ONE_HUNDRED_KILT)]) + .with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID]) + .with_bonded_balance(vec![ + (DEFAULT_COLLATERAL_CURRENCY_ID, pool_id.clone(), u128::MAX / 10), + (DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_00, u128::MAX / 10), + ]) + .build_and_execute_with_sanity_tests(|| { + let origin = RawOrigin::Signed(ACCOUNT_00).into(); + + assert_err!( + BondingPallet::set_lock( + origin, + pool_id.clone(), + Locks { + allow_burn: true, + allow_mint: true + } + ), + Error::::InvalidInput + ); + }); +} + #[test] fn set_lock_fails_when_not_authorized() { let pool_details = generate_pool_details( diff --git a/pallets/pallet-bonded-coins/src/traits.rs b/pallets/pallet-bonded-coins/src/traits.rs index d87176207..65d8fba7a 100644 --- a/pallets/pallet-bonded-coins/src/traits.rs +++ b/pallets/pallet-bonded-coins/src/traits.rs @@ -60,8 +60,10 @@ where } } -/// Copy from the Polkadot SDK. once we are at version 1.13.0, we can remove -/// this. +/// Copy of a trait from a later version of the Polkadot SDK +/// (frame_support::traits::tokens::fungibles::roles::ResetTeam). Once we +/// upgraded to Polkadot SDK version 1.13.0, this can be retired in favor of the +/// original trait. pub trait ResetTeam: Inspect { /// Reset the team for the asset with the given `id`. /// @@ -80,6 +82,10 @@ pub trait ResetTeam: Inspect { ) -> DispatchResult; } +/// Implementation of the back-ported ResetTeam trait for the assets pallet, +/// relying on its `transfer_ownership` and `set_team` calls. Later versions of +/// the assets pallet implement the original trait, so this is a stop-gap +/// solution until we upgraded to at least Polkadot SDK version 1.13.0. impl ResetTeam> for AssetsPallet where T: AssetConfig, @@ -87,14 +93,24 @@ where { fn reset_team( id: Self::AssetId, - _owner: AccountIdOf, + owner: AccountIdOf, admin: AccountIdOf, issuer: AccountIdOf, freezer: AccountIdOf, ) -> DispatchResult { - let owner = AssetsPallet::::owner(id.clone()).ok_or(DispatchError::Unavailable)?; - let origin = RawOrigin::Signed(owner); - AssetsPallet::::set_team(origin.into(), id.into(), issuer.into(), admin.into(), freezer.into()) + let current_owner = AssetsPallet::::owner(id.clone()).ok_or(DispatchError::Unavailable)?; + AssetsPallet::::transfer_ownership( + RawOrigin::Signed(current_owner).into(), + id.clone().into(), + owner.clone().into(), + )?; + AssetsPallet::::set_team( + RawOrigin::Signed(owner).into(), + id.into(), + issuer.into(), + admin.into(), + freezer.into(), + ) } } diff --git a/pallets/pallet-bonded-coins/src/try_state.rs b/pallets/pallet-bonded-coins/src/try_state.rs index b1407bf44..e88481bff 100644 --- a/pallets/pallet-bonded-coins/src/try_state.rs +++ b/pallets/pallet-bonded-coins/src/try_state.rs @@ -18,7 +18,7 @@ pub(crate) fn do_try_state() -> Result<(), TryRuntimeError> { owner, bonded_currencies, state, - denomination, + currencies_settings, .. } = pool_details; @@ -61,7 +61,7 @@ pub(crate) fn do_try_state() -> Result<(), TryRuntimeError> { // The Currency in the fungibles pallet should always match with the // denomination stored in the pool. let currency_denomination = T::Fungibles::decimals(currency_id.clone()); - assert_eq!(currency_denomination, denomination); + assert_eq!(currency_denomination, currencies_settings.denomination); // if currency has on-zero supply -> collateral in pool account must be // non-zero. diff --git a/pallets/pallet-bonded-coins/src/types.rs b/pallets/pallet-bonded-coins/src/types.rs index 48fdf0430..ccdf13fca 100644 --- a/pallets/pallet-bonded-coins/src/types.rs +++ b/pallets/pallet-bonded-coins/src/types.rs @@ -25,6 +25,12 @@ pub struct Locks { pub allow_burn: bool, } +impl Locks { + pub const fn any_lock_set(&self) -> bool { + !(self.allow_mint && self.allow_burn) + } +} + /// Status of a pool. #[derive(Clone, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen, Debug)] pub enum PoolStatus { @@ -72,12 +78,24 @@ impl PoolStatus { } } +#[derive(Clone, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen, Debug, Default)] +pub struct BondedCurrenciesSettings { + /// The minimum amount that can be minted/burnt. + pub min_operation_balance: FungiblesBalance, + /// The denomination of all bonded assets the pool. + pub denomination: u8, + /// Whether asset management team changes are allowed. + pub allow_reset_team: bool, + /// Whether assets are transferable or not. + pub transferable: bool, +} + /// Details of a pool. #[derive(Clone, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen, Debug)] -pub struct PoolDetails { +pub struct PoolDetails { /// The owner of the pool. pub owner: AccountId, - /// The manager of the pool. If a manager is set, the pool is permissioned. + /// The manager of the pool, who can execute privileged transactions. pub manager: Option, /// The curve of the pool. pub curve: ParametrizedCurve, @@ -87,19 +105,21 @@ pub struct PoolDetails, - /// Whether the pool is transferable or not. - pub transferable: bool, - /// The denomination of the pool. - pub denomination: u8, - /// The minimum amount that can be minted/burnt. - pub min_operation_balance: u128, + /// Shared settings of the currencies in the pool. + pub currencies_settings: SharedSettings, /// The deposit to be returned upon destruction of this pool. pub deposit: DepositBalance, } -impl - PoolDetails -where +impl + PoolDetails< + AccountId, + ParametrizedCurve, + Currencies, + BaseCurrencyId, + DepositBalance, + BondedCurrenciesSettings, + > where AccountId: PartialEq + Clone, { #[allow(clippy::too_many_arguments)] @@ -110,8 +130,9 @@ where collateral: BaseCurrencyId, bonded_currencies: Currencies, transferable: bool, + allow_reset_team: bool, denomination: u8, - min_operation_balance: u128, + min_operation_balance: FungiblesBalance, deposit: DepositBalance, ) -> Self { Self { @@ -120,10 +141,13 @@ where curve, collateral, bonded_currencies, - transferable, + currencies_settings: BondedCurrenciesSettings { + transferable, + allow_reset_team, + denomination, + min_operation_balance, + }, state: PoolStatus::default(), - denomination, - min_operation_balance, deposit, } } diff --git a/runtime-api/bonded-coins/src/lib.rs b/runtime-api/bonded-coins/src/lib.rs index 2d6aef18b..349b04b9e 100644 --- a/runtime-api/bonded-coins/src/lib.rs +++ b/runtime-api/bonded-coins/src/lib.rs @@ -44,7 +44,7 @@ pub trait OperationValue { sp_api::decl_runtime_apis! { /// Runtime API to compute the collateral for a given amount and pool ID /// and to query all pool IDs where the given account is the manager or owner. - #[api_version(1)] + #[api_version(2)] pub trait BondedCurrency where Balance: Codec, PoolId: Codec, diff --git a/runtime-api/bonded-coins/src/pool_details.rs b/runtime-api/bonded-coins/src/pool_details.rs index 9bc7ee8dd..a7b501eb9 100644 --- a/runtime-api/bonded-coins/src/pool_details.rs +++ b/runtime-api/bonded-coins/src/pool_details.rs @@ -16,7 +16,7 @@ // If you feel like getting in touch with us, you can do so at -use pallet_bonded_coins::{curves::Curve, Locks, PoolStatus}; +use pallet_bonded_coins::{curves::Curve, BondedCurrenciesSettings, Locks, PoolStatus}; use parity_scale_codec::{alloc::string::String, Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_std::vec::Vec; @@ -35,10 +35,19 @@ pub type PoolDetailsOf = PoolDet CurrenciesOf, CollateralDetails, Balance, + Balance, >; #[derive(Default, Clone, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen, Debug)] -pub struct PoolDetails { +pub struct PoolDetails< + AccountId, + PoolId, + ParametrizedCurve, + Currencies, + BaseCurrencyId, + DepositBalance, + FungiblesBalance, +> { /// The ID of the pool. pub id: PoolId, /// The owner of the pool. @@ -53,12 +62,8 @@ pub struct PoolDetails, - /// Whether the pool is transferable or not. - pub transferable: bool, - /// The denomination of the pool. - pub denomination: u8, - /// The minimum amount that can be minted/burnt. - pub min_operation_balance: u128, + /// Shared settings of the currencies in the pool. + pub currencies_settings: BondedCurrenciesSettings, /// The deposit to be returned upon destruction of this pool. pub deposit: DepositBalance, } diff --git a/runtimes/common/src/constants.rs b/runtimes/common/src/constants.rs index 120f74576..073fe6d83 100644 --- a/runtimes/common/src/constants.rs +++ b/runtimes/common/src/constants.rs @@ -162,7 +162,7 @@ pub mod bonded_coins { use super::*; /// The size is checked in the runtime by a test. - pub const MAX_POOL_BYTE_LENGTH: u32 = 986; + pub const MAX_POOL_BYTE_LENGTH: u32 = 987; pub const BASE_DEPOSIT: Balance = deposit(1, MAX_POOL_BYTE_LENGTH); const ASSET_ID_BYTE_LENGTH: u32 = 8; /// https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/assets/src/types.rs#L188 diff --git a/runtimes/peregrine/src/migrations/mod.rs b/runtimes/peregrine/src/migrations/mod.rs index f6ca110fa..76762e1c0 100644 --- a/runtimes/peregrine/src/migrations/mod.rs +++ b/runtimes/peregrine/src/migrations/mod.rs @@ -32,6 +32,7 @@ pub type RuntimeMigrations = ( frame_support::migrations::RemovePallet::DbWeight>, frame_support::migrations::RemovePallet::DbWeight>, frame_support::migrations::RemovePallet::DbWeight>, + pallet_bonded_coins::migrations::v1::MigrateV0ToV1, ); impl pallet_migration::Config for Runtime { diff --git a/runtimes/peregrine/src/runtime_apis.rs b/runtimes/peregrine/src/runtime_apis.rs index ee156303d..3512e8904 100644 --- a/runtimes/peregrine/src/runtime_apis.rs +++ b/runtimes/peregrine/src/runtime_apis.rs @@ -499,7 +499,8 @@ impl_runtime_apis! { rounding: Round, ) -> Result { let pool = Pools::::get(pool_id).ok_or(BondedCurrencyError::PoolNotFound)?; - let PoolDetailsOf:: { curve, bonded_currencies, denomination, collateral, .. } = pool; + let PoolDetailsOf:: { curve, bonded_currencies, currencies_settings, collateral, .. } = pool; + let denomination = currencies_settings.denomination; let collateral_denomination = NativeAndForeignAssets::decimals(collateral); let normalized_low = balance_to_fixed(low, denomination, rounding).map_err(|_| BondedCurrencyError::BalanceConversion)?; @@ -552,15 +553,13 @@ impl_runtime_apis! { let pool = Pools::::get(&pool_id).ok_or(BondedCurrencyError::PoolNotFound)?; let PoolDetailsOf:: { curve, + currencies_settings, bonded_currencies, owner, collateral, - denomination, deposit, - min_operation_balance, manager, state, - transferable, } = pool; let currencies = bonded_currencies.iter().map(|currency_id| -> Result, BondedCurrencyError> { @@ -604,12 +603,10 @@ impl_runtime_apis! { curve: fmt_curve, collateral: collateral_details, owner, - denomination, deposit, - min_operation_balance, manager, state, - transferable, + currencies_settings }) } diff --git a/runtimes/peregrine/src/system/mod.rs b/runtimes/peregrine/src/system/mod.rs index e8aab090b..27cf37e53 100644 --- a/runtimes/peregrine/src/system/mod.rs +++ b/runtimes/peregrine/src/system/mod.rs @@ -93,7 +93,7 @@ impl frame_system::Config for Runtime { type SS58Prefix = ConstU16; /// The set code logic type OnSetCode = cumulus_pallet_parachain_system::ParachainSetCode; - type MaxConsumers = frame_support::traits::ConstU32<16>; + type MaxConsumers = frame_support::traits::ConstU32<100>; } impl pallet_timestamp::Config for Runtime { diff --git a/runtimes/spiritnet/src/system/mod.rs b/runtimes/spiritnet/src/system/mod.rs index 33a953890..42f495fcb 100644 --- a/runtimes/spiritnet/src/system/mod.rs +++ b/runtimes/spiritnet/src/system/mod.rs @@ -93,7 +93,7 @@ impl frame_system::Config for Runtime { type SS58Prefix = ConstU16; /// The set code logic type OnSetCode = cumulus_pallet_parachain_system::ParachainSetCode; - type MaxConsumers = frame_support::traits::ConstU32<16>; + type MaxConsumers = frame_support::traits::ConstU32<100>; } impl pallet_timestamp::Config for Runtime {