Skip to content

Commit

Permalink
fix: change premium calculation flow
Browse files Browse the repository at this point in the history
  • Loading branch information
nakul1010 committed Dec 6, 2023
1 parent 3dfeb00 commit 5a00bac
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 103 deletions.
17 changes: 5 additions & 12 deletions crates/fee/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -429,17 +427,12 @@ impl<T: Config> Pallet<T> {
amount.checked_rounded_mul(&<PremiumRedeemFee<T>>::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<T>,
) -> Result<UnsignedFixedPoint<T>, DispatchError> {
Ok(amount
.checked_sub(&<PremiumRedeemFee<T>>::get())
.ok_or(Error::<T>::PremiumRedeemSubtractionFailed)?)
/// # Returns
/// Returns the premium redeem reward rate.
pub fn premium_redeem_reward_rate() -> UnsignedFixedPoint<T> {
<PremiumRedeemFee<T>>::get()
}

/// Calculate punishment fee for a Vault that fails to execute a redeem
Expand Down
2 changes: 1 addition & 1 deletion crates/issue/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub(crate) mod vault_registry {
}

pub fn ensure_not_banned<T: crate::Config>(vault_id: &DefaultVaultId<T>) -> DispatchResult {
<vault_registry::Pallet<T>>::_ensure_not_banned(vault_id)
<vault_registry::Pallet<T>>::ensure_not_banned(vault_id)
}

pub fn decrease_to_be_issued_tokens<T: crate::Config>(
Expand Down
38 changes: 8 additions & 30 deletions crates/redeem/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: crate::Config>(vault_id: &DefaultVaultId<T>) -> Result<Amount<T>, DispatchError> {
<vault_registry::Pallet<T>>::get_backing_collateral(vault_id)
pub fn get_vault_max_premium_redeem<T: crate::Config>(
vault_id: &DefaultVaultId<T>,
) -> Result<Amount<T>, DispatchError> {
<vault_registry::Pallet<T>>::get_vault_max_premium_redeem(vault_id)
}

pub fn get_liquidated_collateral<T: crate::Config>(
Expand Down Expand Up @@ -80,18 +82,6 @@ pub(crate) mod vault_registry {
<vault_registry::Pallet<T>>::get_vault_from_id(vault_id)
}

pub fn vault_to_be_backed_tokens<T: crate::Config>(
vault_id: &DefaultVaultId<T>,
) -> Result<Amount<T>, DispatchError> {
<vault_registry::Pallet<T>>::vault_to_be_backed_tokens(vault_id)
}

pub fn vault_capacity_at_secure_threshold<T: crate::Config>(
vault_id: &DefaultVaultId<T>,
) -> Result<Amount<T>, DispatchError> {
<vault_registry::Pallet<T>>::vault_capacity_at_secure_threshold(vault_id)
}

pub fn try_increase_to_be_redeemed_tokens<T: crate::Config>(
vault_id: &DefaultVaultId<T>,
amount: &Amount<T>,
Expand Down Expand Up @@ -136,7 +126,7 @@ pub(crate) mod vault_registry {
}

pub fn ensure_not_banned<T: crate::Config>(vault_id: &DefaultVaultId<T>) -> DispatchResult {
<vault_registry::Pallet<T>>::_ensure_not_banned(vault_id)
<vault_registry::Pallet<T>>::ensure_not_banned(vault_id)
}

pub fn is_vault_below_premium_threshold<T: crate::Config>(
Expand Down Expand Up @@ -183,12 +173,6 @@ pub(crate) mod vault_registry {
) -> Result<(Amount<T>, Amount<T>), DispatchError> {
<vault_registry::Pallet<T>>::decrease_to_be_replaced_tokens(vault_id, tokens)
}

pub fn get_secure_threshold<T: crate::Config>(
vault_id: &DefaultVaultId<T>,
) -> Result<UnsignedFixedPoint<T>, DispatchError> {
<vault_registry::Pallet<T>>::get_secure_threshold(vault_id)
}
}

#[cfg_attr(test, mockable)]
Expand Down Expand Up @@ -248,13 +232,7 @@ pub(crate) mod fee {
<fee::Pallet<T>>::get_punishment_fee(amount)
}

pub fn get_premium_redeem_fee<T: crate::Config>(amount: &Amount<T>) -> Result<Amount<T>, DispatchError> {
<fee::Pallet<T>>::get_premium_redeem_fee(amount)
}

pub fn apply_premium_redeem_discount<T: crate::Config>(
amount: &UnsignedFixedPoint<T>,
) -> Result<UnsignedFixedPoint<T>, DispatchError> {
<fee::Pallet<T>>::apply_premium_redeem_discount(amount)
pub fn premium_redeem_reward_rate<T: crate::Config>() -> UnsignedFixedPoint<T> {
<fee::Pallet<T>>::premium_redeem_reward_rate()
}
}
32 changes: 6 additions & 26 deletions crates/redeem/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,33 +506,13 @@ impl<T: Config> Pallet<T> {
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::<T>();
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::<T>(&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::<T>(&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::<T>(
&token_exchange_value.saturating_sub(&backing_collateral)?, // (oldCol - oldTokens * EXCH * SECURE)
)? // FEE * (oldTokens * EXCH * SECURE - oldCol))
.checked_div(&ext::fee::apply_premium_redeem_discount::<T>(&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)
};
Expand Down
2 changes: 1 addition & 1 deletion crates/replace/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub(crate) mod vault_registry {
}

pub fn ensure_not_banned<T: crate::Config>(vault_id: &DefaultVaultId<T>) -> DispatchResult {
<vault_registry::Pallet<T>>::_ensure_not_banned(vault_id)
<vault_registry::Pallet<T>>::ensure_not_banned(vault_id)
}

pub fn try_increase_to_be_issued_tokens<T: crate::Config>(
Expand Down
5 changes: 5 additions & 0 deletions crates/vault-registry/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: crate::Config>(vault_id: &DefaultVaultId<T>) -> DispatchResult {
<fee::Pallet<T>>::distribute_all_vault_rewards(vault_id)
}

pub fn premium_redeem_reward_rate<T: crate::Config>() -> UnsignedFixedPoint<T> {
<fee::Pallet<T>>::premium_redeem_reward_rate()
}
}
101 changes: 86 additions & 15 deletions crates/vault-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,52 @@ impl<T: Config> Pallet<T> {
ext::staking::total_current_stake::<T>(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<T>`.
pub fn get_vault_max_premium_redeem(vault_id: &DefaultVaultId<T>) -> Result<Amount<T>, 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::<T>();

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(&current_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<T>) -> Result<Amount<T>, DispatchError> {
let vault = Self::get_vault_from_id(vault_id)?;
Ok(Amount::new(vault.liquidated_collateral, vault_id.currencies.collateral))
Expand Down Expand Up @@ -1089,18 +1135,40 @@ impl<T: Config> Pallet<T> {
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<T>) -> Result<UnsignedFixedPoint<T>, 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<CurrencyId<T>>,
) -> Result<UnsignedFixedPoint<T>, DispatchError> {
let global_secure_threshold =
Self::secure_collateral_threshold(&currency_pair).ok_or(Error::<T>::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<T>,
to_be_backed_tokens: &Amount<T>,
secure_threshold: UnsignedFixedPoint<T>,
) -> Result<Amount<T>, 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.
Expand Down Expand Up @@ -1496,7 +1564,7 @@ impl<T: Config> Pallet<T> {
Ok(())
}

pub fn _ensure_not_banned(vault_id: &DefaultVaultId<T>) -> DispatchResult {
pub fn ensure_not_banned(vault_id: &DefaultVaultId<T>) -> DispatchResult {
let vault = Self::get_active_rich_vault_from_id(&vault_id)?;
vault.ensure_not_banned()
}
Expand Down Expand Up @@ -1614,13 +1682,16 @@ impl<T: Config> Pallet<T> {
/// The redeemable tokens are the currently vault.issued_tokens - the vault.to_be_redeemed_tokens
pub fn get_premium_redeem_vaults() -> Result<Vec<(DefaultVaultId<T>, Amount<T>)>, DispatchError> {
let mut suitable_vaults = Vaults::<T>::iter()
.filter_map(|(vault_id, vault)| {
let rich_vault: RichVault<T> = 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
}
Expand Down
38 changes: 23 additions & 15 deletions crates/vault-registry/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>.mock_safe(move || MockResult::Return(1.into()));

test()
})
Expand All @@ -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!(
Expand All @@ -1036,40 +1041,43 @@ 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))])
);
})
}

#[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)))));
})
}
}
Expand Down
Loading

0 comments on commit 5a00bac

Please sign in to comment.