diff --git a/crates/fee/src/lib.rs b/crates/fee/src/lib.rs index 96523f0149..c172324475 100644 --- a/crates/fee/src/lib.rs +++ b/crates/fee/src/lib.rs @@ -111,8 +111,6 @@ pub mod pallet { TryIntoIntError, /// Value exceeds the expected upper bound for storage fields in this pallet. AboveMaxExpectedValue, - /// Subtraction of the premium redeem fee from a value failed. - PremiumRedeemSubtractionFailed, } #[pallet::hooks] @@ -429,17 +427,12 @@ impl Pallet { amount.checked_rounded_mul(&>::get(), Rounding::NearestPrefUp) } - /// Apply a premium redeem discount to the given unsigned fixed-point value + /// Get the premium redeem reward rate. /// - /// # Arguments - /// - /// * `amount` - amount in collateral (at current exchange rate) - pub fn apply_premium_redeem_discount( - amount: &UnsignedFixedPoint, - ) -> Result, DispatchError> { - Ok(amount - .checked_sub(&>::get()) - .ok_or(Error::::PremiumRedeemSubtractionFailed)?) + /// # Returns + /// Returns the premium redeem reward rate. + pub fn premium_redeem_reward_rate() -> UnsignedFixedPoint { + >::get() } /// Calculate punishment fee for a Vault that fails to execute a redeem diff --git a/crates/issue/src/ext.rs b/crates/issue/src/ext.rs index d548bb6b9f..9a149a236a 100644 --- a/crates/issue/src/ext.rs +++ b/crates/issue/src/ext.rs @@ -95,7 +95,7 @@ pub(crate) mod vault_registry { } pub fn ensure_not_banned(vault_id: &DefaultVaultId) -> DispatchResult { - >::_ensure_not_banned(vault_id) + >::ensure_not_banned(vault_id) } pub fn decrease_to_be_issued_tokens( diff --git a/crates/redeem/src/ext.rs b/crates/redeem/src/ext.rs index 9d9cc1f7ba..32a0f25ff7 100644 --- a/crates/redeem/src/ext.rs +++ b/crates/redeem/src/ext.rs @@ -42,10 +42,12 @@ pub(crate) mod vault_registry { use crate::DefaultVaultId; use currency::Amount; use frame_support::dispatch::{DispatchError, DispatchResult}; - use vault_registry::types::{CurrencyId, CurrencySource, DefaultVault, UnsignedFixedPoint}; + use vault_registry::types::{CurrencyId, CurrencySource, DefaultVault}; - pub fn get_backing_collateral(vault_id: &DefaultVaultId) -> Result, DispatchError> { - >::get_backing_collateral(vault_id) + pub fn get_vault_max_premium_redeem( + vault_id: &DefaultVaultId, + ) -> Result, DispatchError> { + >::get_vault_max_premium_redeem(vault_id) } pub fn get_liquidated_collateral( @@ -80,18 +82,6 @@ pub(crate) mod vault_registry { >::get_vault_from_id(vault_id) } - pub fn vault_to_be_backed_tokens( - vault_id: &DefaultVaultId, - ) -> Result, DispatchError> { - >::vault_to_be_backed_tokens(vault_id) - } - - pub fn vault_capacity_at_secure_threshold( - vault_id: &DefaultVaultId, - ) -> Result, DispatchError> { - >::vault_capacity_at_secure_threshold(vault_id) - } - pub fn try_increase_to_be_redeemed_tokens( vault_id: &DefaultVaultId, amount: &Amount, @@ -136,7 +126,7 @@ pub(crate) mod vault_registry { } pub fn ensure_not_banned(vault_id: &DefaultVaultId) -> DispatchResult { - >::_ensure_not_banned(vault_id) + >::ensure_not_banned(vault_id) } pub fn is_vault_below_premium_threshold( @@ -183,12 +173,6 @@ pub(crate) mod vault_registry { ) -> Result<(Amount, Amount), DispatchError> { >::decrease_to_be_replaced_tokens(vault_id, tokens) } - - pub fn get_secure_threshold( - vault_id: &DefaultVaultId, - ) -> Result, DispatchError> { - >::get_secure_threshold(vault_id) - } } #[cfg_attr(test, mockable)] @@ -248,13 +232,7 @@ pub(crate) mod fee { >::get_punishment_fee(amount) } - pub fn get_premium_redeem_fee(amount: &Amount) -> Result, DispatchError> { - >::get_premium_redeem_fee(amount) - } - - pub fn apply_premium_redeem_discount( - amount: &UnsignedFixedPoint, - ) -> Result, DispatchError> { - >::apply_premium_redeem_discount(amount) + pub fn premium_redeem_reward_rate() -> UnsignedFixedPoint { + >::premium_redeem_reward_rate() } } diff --git a/crates/redeem/src/lib.rs b/crates/redeem/src/lib.rs index f0e2c50396..34f4568d84 100644 --- a/crates/redeem/src/lib.rs +++ b/crates/redeem/src/lib.rs @@ -506,33 +506,13 @@ impl Pallet { let currency_id = vault_id.collateral_currency(); let premium_collateral = if below_premium_redeem { - // Calculate the secure vault capacity - let secure_vault_capacity = ext::vault_registry::vault_capacity_at_secure_threshold(&vault_id)?; - let to_be_backed_tokens = ext::vault_registry::vault_to_be_backed_tokens(&vault_id)?; - let difference_in_tokens_to_reach_secure_threshold = - to_be_backed_tokens.saturating_sub(&secure_vault_capacity)?; + let redeem_amount_wrapped_in_collateral = user_to_be_received_btc.convert_to(currency_id)?; + let premium_redeem_rate = ext::fee::premium_redeem_reward_rate::(); + let premium_for_redeem_amount = redeem_amount_wrapped_in_collateral + .checked_rounded_mul(&premium_redeem_rate, Rounding::NearestPrefUp)?; - if difference_in_tokens_to_reach_secure_threshold.gt(&user_to_be_received_btc)? { - let premium_tokens_in_collateral = user_to_be_received_btc.convert_to(currency_id)?; - - ext::fee::get_premium_redeem_fee::(&premium_tokens_in_collateral)? - } else { - // Formula = max_premium_collateral = (FEE * (oldTokens * EXCH * SECURE - oldCol)) / (SECURE - FEE) - - let backing_collateral = ext::vault_registry::get_backing_collateral(&vault_id)?; - - let secure_threshold = ext::vault_registry::get_secure_threshold::(&vault_id)?; - - let issued_tokens_in_collateral = to_be_backed_tokens.convert_to(currency_id)?; // oldTokens * EXCH - - let token_exchange_value = - issued_tokens_in_collateral.checked_rounded_mul(&secure_threshold, Rounding::NearestPrefUp)?; // oldTokens * EXCH * SECURE - - ext::fee::get_premium_redeem_fee::( - &token_exchange_value.saturating_sub(&backing_collateral)?, // (oldCol - oldTokens * EXCH * SECURE) - )? // FEE * (oldTokens * EXCH * SECURE - oldCol)) - .checked_div(&ext::fee::apply_premium_redeem_discount::(&secure_threshold)?)? // (SECURE - FEE) - } + let max_premium = ext::vault_registry::get_vault_max_premium_redeem(&vault_id)?; + max_premium.min(&premium_for_redeem_amount)? } else { Amount::zero(currency_id) }; diff --git a/crates/replace/src/ext.rs b/crates/replace/src/ext.rs index 730d59c8cb..62457cfeba 100644 --- a/crates/replace/src/ext.rs +++ b/crates/replace/src/ext.rs @@ -81,7 +81,7 @@ pub(crate) mod vault_registry { } pub fn ensure_not_banned(vault_id: &DefaultVaultId) -> DispatchResult { - >::_ensure_not_banned(vault_id) + >::ensure_not_banned(vault_id) } pub fn try_increase_to_be_issued_tokens( diff --git a/crates/vault-registry/src/ext.rs b/crates/vault-registry/src/ext.rs index 72a3571cf4..83494f4675 100644 --- a/crates/vault-registry/src/ext.rs +++ b/crates/vault-registry/src/ext.rs @@ -113,9 +113,14 @@ pub(crate) mod capacity { #[cfg_attr(test, mockable)] pub(crate) mod fee { use crate::DefaultVaultId; + use fee::types::UnsignedFixedPoint; use frame_support::dispatch::DispatchResult; pub fn distribute_all_vault_rewards(vault_id: &DefaultVaultId) -> DispatchResult { >::distribute_all_vault_rewards(vault_id) } + + pub fn premium_redeem_reward_rate() -> UnsignedFixedPoint { + >::premium_redeem_reward_rate() + } } diff --git a/crates/vault-registry/src/lib.rs b/crates/vault-registry/src/lib.rs index e04e53f91c..98a6f2a7f7 100644 --- a/crates/vault-registry/src/lib.rs +++ b/crates/vault-registry/src/lib.rs @@ -774,6 +774,52 @@ impl Pallet { ext::staking::total_current_stake::(vault_id) } + /// Calculate the maximum premium that can be given by a vault. + /// + /// # Arguments + /// * `vault_id` - The identifier of the vault for which the maximum premium is being calculated. + /// + /// # Returns + /// Returns a `Result` containing the calculated maximum premium as an `Amount`. + pub fn get_vault_max_premium_redeem(vault_id: &DefaultVaultId) -> Result, DispatchError> { + // The goal of premium redeems is to get the vault back the a healthy collateralization ratio. As such, + // we only award a premium for the amount of tokens required to get the vault back to secure threshold. + + // The CollateralizationRate is defined as `totalCollateral / convertToCollateral(totalTokens)` + // When paying a premium, the collateralization rate gets updated according to the following formula: + // `NewCollateralization = (oldCol - awardedPremium) / ( oldTokens*EXCH - awardedPremium/FEE)` + + // To calculate the maximum premium we are willing to pay, we set the newCollateralization to + // the global secure threshold, which gives: + // `SECURE = (oldCol - awardedPremium) / (oldTokens*EXCH - awardedPremium/FEE)`` + // We can rewrite this formula to calculate the `premium` amount that would get us to the secure + // threshold: `maxPremium = (oldTokens * EXCH * SECURE - oldCol) * (FEE / (SECURE - + // FEE))` Which can be interpreted as: + // `maxPremium = missingCollateral * (FEE / (SECURE - FEE)) + + // Note that to prevent repeated premium redeems while waiting for execution, we use to_be_backed_tokens + // for `oldCol`, which takes into account pending issues and redeems + let to_be_backed_tokens = Self::vault_to_be_backed_tokens(&vault_id)?; + let global_secure_threshold = Self::get_global_secure_threshold(&vault_id.currencies)?; + let premium_redeem_rate = ext::fee::premium_redeem_reward_rate::(); + + let required_collateral = Self::required_collateral(&vault_id, &to_be_backed_tokens, global_secure_threshold)?; + + let current_collateral = Self::get_backing_collateral(&vault_id)?; + let missing_collateral = required_collateral.checked_sub(¤t_collateral)?; + + let factor = premium_redeem_rate + .checked_div( + &global_secure_threshold + .checked_sub(&premium_redeem_rate) + .ok_or(ArithmeticError::Underflow)?, + ) + .ok_or(ArithmeticError::DivisionByZero)?; + + let max_premium = missing_collateral.checked_mul(&factor)?; + Ok(max_premium) + } + pub fn get_liquidated_collateral(vault_id: &DefaultVaultId) -> Result, DispatchError> { let vault = Self::get_vault_from_id(vault_id)?; Ok(Amount::new(vault.liquidated_collateral, vault_id.currencies.collateral)) @@ -1089,18 +1135,40 @@ impl Pallet { Ok(()) } - /// Get the secure threshold for a specified vault. + /// Get the global secure threshold for a specified currency pair. + /// /// # Arguments - /// * `vault_id` - the id of the vault from which to issue tokens + /// * `currency_pair` - The currency pair for which to retrieve the global secure threshold. /// /// # Returns - /// Returns the secure threshold of the specified vault or an error if the vault retrieval fails. + /// Returns the global secure threshold for the specified currency pair or an error if the threshold is not set. /// /// # Errors - /// * `VaultNotFound` - if no vault exists for the given `vault_id` - pub fn get_secure_threshold(vault_id: &DefaultVaultId) -> Result, DispatchError> { - let vault = Self::get_rich_vault_from_id(&vault_id)?; - vault.get_secure_threshold() + /// * `ThresholdNotSet` - If the secure collateral threshold for the given `currency_pair` is not set. + pub fn get_global_secure_threshold( + currency_pair: &VaultCurrencyPair>, + ) -> Result, DispatchError> { + let global_secure_threshold = + Self::secure_collateral_threshold(¤cy_pair).ok_or(Error::::ThresholdNotSet)?; + Ok(global_secure_threshold) + } + + /// Calculate the required collateral for a vault given the specified parameters. + /// + /// # Arguments + /// * `vault_id` - The identifier of the vault for which to calculate the required collateral. + /// * `to_be_backed_tokens` - The amount of tokens to be backed by collateral. + /// * `secure_threshold` - The secure collateral threshold to be applied in the calculation. + /// + /// # Returns + /// Returns the required collateral amount or an error if the calculation fails. + pub fn required_collateral( + vault_id: &DefaultVaultId, + to_be_backed_tokens: &Amount, + secure_threshold: UnsignedFixedPoint, + ) -> Result, DispatchError> { + let issued_tokens_in_collateral = to_be_backed_tokens.convert_to(vault_id.collateral_currency())?; // oldTokens * EXCH + issued_tokens_in_collateral.checked_rounded_mul(&secure_threshold, Rounding::NearestPrefUp) } /// Adds an amount tokens to the to-be-redeemed tokens balance of a vault. @@ -1496,7 +1564,7 @@ impl Pallet { Ok(()) } - pub fn _ensure_not_banned(vault_id: &DefaultVaultId) -> DispatchResult { + pub fn ensure_not_banned(vault_id: &DefaultVaultId) -> DispatchResult { let vault = Self::get_active_rich_vault_from_id(&vault_id)?; vault.ensure_not_banned() } @@ -1614,13 +1682,16 @@ impl Pallet { /// The redeemable tokens are the currently vault.issued_tokens - the vault.to_be_redeemed_tokens pub fn get_premium_redeem_vaults() -> Result, Amount)>, DispatchError> { let mut suitable_vaults = Vaults::::iter() - .filter_map(|(vault_id, vault)| { - let rich_vault: RichVault = vault.into(); - - let redeemable_tokens = rich_vault.redeemable_tokens().ok()?; - - if !redeemable_tokens.is_zero() && Self::is_vault_below_premium_threshold(&vault_id).unwrap_or(false) { - Some((vault_id, redeemable_tokens)) + .filter_map(|(vault_id, _vault)| { + let max_premium_in_collateral = Self::get_vault_max_premium_redeem(&vault_id).ok()?; + let premium_redeemable_tokens = + max_premium_in_collateral.convert_to(vault_id.wrapped_currency()).ok()?; + + if Self::ensure_not_banned(&vault_id).is_ok() + && !premium_redeemable_tokens.is_zero() + && Self::is_vault_below_premium_threshold(&vault_id).unwrap_or(false) + { + Some((vault_id, premium_redeemable_tokens)) } else { None } diff --git a/crates/vault-registry/src/tests.rs b/crates/vault-registry/src/tests.rs index cd1dd05cc2..bb1defa353 100644 --- a/crates/vault-registry/src/tests.rs +++ b/crates/vault-registry/src/tests.rs @@ -987,12 +987,14 @@ fn get_settled_collateralization_from_vault_succeeds() { mod get_vaults_below_premium_collaterlization_tests { use super::{assert_eq, *}; + use crate::ext; /// sets premium_redeem threshold to 1 pub fn run_test(test: impl FnOnce()) { super::run_test(|| { VaultRegistry::_set_secure_collateral_threshold(DEFAULT_CURRENCY_PAIR, FixedU128::from_float(0.001)); VaultRegistry::_set_premium_redeem_threshold(DEFAULT_CURRENCY_PAIR, FixedU128::one()); + ext::fee::premium_redeem_reward_rate::.mock_safe(move || MockResult::Return(1.into())); test() }) @@ -1013,6 +1015,9 @@ mod get_vaults_below_premium_collaterlization_tests { #[test] fn get_vaults_below_premium_collateralization_fails() { run_test(|| { + // set back to default threshold + set_default_thresholds(); + add_vault(vault_id(4), 50, 100); assert_err!( @@ -1036,9 +1041,12 @@ mod get_vaults_below_premium_collaterlization_tests { add_vault(id1.clone(), issue_tokens1, collateral1); add_vault(id2.clone(), issue_tokens2, collateral2); + // set back to default threshold so that vaults fall under premium redeem + set_default_thresholds(); + assert_eq!( VaultRegistry::get_premium_redeem_vaults(), - Ok(vec![(id1, wrapped(issue_tokens1)), (id2, wrapped(issue_tokens2))]) + Ok(vec![(id2, wrapped(52)), (id1, wrapped(51))]) ); }) } @@ -1046,30 +1054,30 @@ mod get_vaults_below_premium_collaterlization_tests { #[test] fn get_vaults_below_premium_collateralization_filters_banned_and_sufficiently_collateralized_vaults() { run_test(|| { - // not returned, because is is not under premium threshold (which is set to 100% for this test) + // returned let id1 = vault_id(3); let issue_tokens1: u128 = 50; let collateral1 = 50; add_vault(id1.clone(), issue_tokens1, collateral1); - // returned - let id2 = vault_id(4); - let issue_tokens2: u128 = 50; - let collateral2 = 49; - add_vault(id2.clone(), issue_tokens2, collateral2); - // not returned because it's banned - let id3 = vault_id(5); + let id2 = vault_id(5); let issue_tokens3: u128 = 50; let collateral3 = 49; - add_vault(id3.clone(), issue_tokens3, collateral3); - let mut vault3 = VaultRegistry::get_active_rich_vault_from_id(&id3).unwrap(); + add_vault(id2.clone(), issue_tokens3, collateral3); + let mut vault3 = VaultRegistry::get_active_rich_vault_from_id(&id2).unwrap(); vault3.ban_until(1000); - assert_eq!( - VaultRegistry::get_premium_redeem_vaults(), - Ok(vec!((id2, wrapped(issue_tokens2)))) - ); + // set back to default threshold so that vaults fall under premium redeem + set_default_thresholds(); + + // not returned + let id3 = vault_id(4); + let issue_tokens2: u128 = 50; + let collateral2 = 150; + add_vault(id3.clone(), issue_tokens2, collateral2); + + assert_eq!(VaultRegistry::get_premium_redeem_vaults(), Ok(vec!((id1, wrapped(50))))); }) } } diff --git a/parachain/runtime/runtime-tests/src/parachain/redeem.rs b/parachain/runtime/runtime-tests/src/parachain/redeem.rs index 8ac17cc4bc..533746af51 100644 --- a/parachain/runtime/runtime-tests/src/parachain/redeem.rs +++ b/parachain/runtime/runtime-tests/src/parachain/redeem.rs @@ -872,7 +872,7 @@ mod spec_based_tests { ); check_redeem_status(USER, RedeemRequestStatus::Reimbursed(true)); assert_noop!( - VaultRegistryPallet::_ensure_not_banned(&vault_id), + VaultRegistryPallet::ensure_not_banned(&vault_id), VaultRegistryError::VaultBanned ); }); @@ -1005,7 +1005,7 @@ mod spec_based_tests { }) ); assert_noop!( - VaultRegistryPallet::_ensure_not_banned(&vault_id), + VaultRegistryPallet::ensure_not_banned(&vault_id), VaultRegistryError::VaultBanned ); }); @@ -1347,7 +1347,7 @@ fn integration_test_execute_redeem_on_banned_vault_succeeds() { // should now be banned assert_noop!( - VaultRegistryPallet::_ensure_not_banned(&vault_id), + VaultRegistryPallet::ensure_not_banned(&vault_id), VaultRegistryError::VaultBanned );