Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: only award redeem premium upto the secure threshold #1201

Merged
merged 20 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/fee/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,14 @@ impl<T: Config> Pallet<T> {
amount.checked_rounded_mul(&<PremiumRedeemFee<T>>::get(), Rounding::NearestPrefUp)
}

/// Get the premium redeem reward rate.
///
/// # 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
/// request before the expiry.
///
Expand Down
2 changes: 1 addition & 1 deletion crates/fee/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use scale_info::TypeInfo;

pub(crate) type BalanceOf<T> = <T as currency::Config>::Balance;

pub(crate) type UnsignedFixedPoint<T> = <T as currency::Config>::UnsignedFixedPoint;
pub type UnsignedFixedPoint<T> = <T as currency::Config>::UnsignedFixedPoint;

pub(crate) type DefaultVaultId<T> = VaultId<<T as frame_system::Config>::AccountId, CurrencyId<T>>;

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
7 changes: 7 additions & 0 deletions crates/redeem/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ pub mod benchmarks {

#[extrinsic_call]
_(RawOrigin::Signed(caller), amount, btc_address, vault_id.clone());
let redeem_vault_request = Redeem::<T>::get_redeem_requests_for_vault(vault_id.account_id.clone());
let redeem_request_hash = redeem_vault_request
.first()
.cloned()
.unwrap_or_else(|| panic!("No redeem request found"));
let redeem_struct = RedeemRequests::<T>::get(redeem_request_hash).unwrap();
assert!(redeem_struct.premium > 0);
}

#[benchmark]
Expand Down
1,321 changes: 657 additions & 664 deletions crates/redeem/src/default_weights.rs

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions crates/redeem/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ pub(crate) mod vault_registry {
use frame_support::dispatch::{DispatchError, DispatchResult};
use vault_registry::types::{CurrencyId, CurrencySource, DefaultVault};

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>(
vault_id: &DefaultVaultId<T>,
) -> Result<Amount<T>, DispatchError> {
Expand Down Expand Up @@ -120,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 @@ -207,6 +213,7 @@ pub(crate) mod oracle {
#[cfg_attr(test, mockable)]
pub(crate) mod fee {
use currency::Amount;
use fee::types::UnsignedFixedPoint;
use frame_support::dispatch::{DispatchError, DispatchResult};

pub fn fee_pool_account_id<T: crate::Config>() -> T::AccountId {
Expand All @@ -225,7 +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 premium_redeem_reward_rate<T: crate::Config>() -> UnsignedFixedPoint<T> {
<fee::Pallet<T>>::premium_redeem_reward_rate()
}
}
24 changes: 15 additions & 9 deletions crates/redeem/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub use crate::types::{DefaultRedeemRequest, RedeemRequest, RedeemRequestStatus}
use crate::types::{BalanceOf, RedeemRequestExt, Version};
use bitcoin::types::FullTransactionProof;
use btc_relay::BtcAddress;
use currency::Amount;
use currency::{Amount, Rounding};
use frame_support::{
dispatch::{DispatchError, DispatchResult},
ensure,
Expand Down Expand Up @@ -456,6 +456,7 @@ mod self_redeem {
Ok(())
}
}

// "Internal" functions, callable by code.
#[cfg_attr(test, mockable)]
impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -501,23 +502,28 @@ impl<T: Config> Pallet<T> {
Error::<T>::AmountBelowDustAmount
);

// vault will get rid of the btc + btc_inclusion_fee
ext::vault_registry::try_increase_to_be_redeemed_tokens::<T>(&vault_id, &vault_to_be_burned_tokens)?;

// lock full amount (inc. fee)
amount_wrapped.lock_on(&redeemer)?;
let redeem_id = ext::security::get_secure_id::<T>(&redeemer);

let below_premium_redeem = ext::vault_registry::is_vault_below_premium_threshold::<T>(&vault_id)?;
let currency_id = vault_id.collateral_currency();

let premium_collateral = if below_premium_redeem {
let redeem_amount_wrapped_in_collateral = user_to_be_received_btc.convert_to(currency_id)?;
ext::fee::get_premium_redeem_fee::<T>(&redeem_amount_wrapped_in_collateral)?
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)?;
sander2 marked this conversation as resolved.
Show resolved Hide resolved

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)
};

// vault will get rid of the btc + btc_inclusion_fee
ext::vault_registry::try_increase_to_be_redeemed_tokens::<T>(&vault_id, &vault_to_be_burned_tokens)?;
nakul1010 marked this conversation as resolved.
Show resolved Hide resolved

// lock full amount (inc. fee)
amount_wrapped.lock_on(&redeemer)?;
let redeem_id = ext::security::get_secure_id::<T>(&redeemer);

Self::release_replace_collateral(&vault_id, &vault_to_be_burned_tokens)?;

Self::insert_redeem_request(
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()
}
}
112 changes: 103 additions & 9 deletions crates/vault-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,53 @@ 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.
nakul1010 marked this conversation as resolved.
Show resolved Hide resolved

// 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 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.saturating_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,6 +1136,42 @@ impl<T: Config> Pallet<T> {
Ok(())
}

/// Get the global secure threshold for a specified currency pair.
///
/// # Arguments
/// * `currency_pair` - The currency pair for which to retrieve the global secure threshold.
///
/// # Returns
/// Returns the global secure threshold for the specified currency pair or an error if the threshold is not set.
///
/// # Errors
/// * `ThresholdNotSet` - If the secure collateral threshold for the given `currency_pair` is not set.
pub fn get_global_secure_threshold(
nakul1010 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
nakul1010 marked this conversation as resolved.
Show resolved Hide resolved

/// Adds an amount tokens to the to-be-redeemed tokens balance of a vault.
/// This function serves as a prevention against race conditions in the
/// redeem and replace procedures. If, for example, a vault would receive
Expand Down Expand Up @@ -1482,7 +1565,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 All @@ -1499,8 +1582,11 @@ impl<T: Config> Pallet<T> {
}

pub fn is_vault_below_premium_threshold(vault_id: &DefaultVaultId<T>) -> Result<bool, DispatchError> {
let vault = Self::get_rich_vault_from_id(&vault_id)?;
let threshold = Self::premium_redeem_threshold(&vault_id.currencies).ok_or(Error::<T>::ThresholdNotSet)?;
Self::is_vault_below_threshold(vault_id, threshold)
let collateral = Self::get_backing_collateral(vault_id)?;

Self::is_collateral_below_threshold(&collateral, &vault.to_be_backed_tokens()?, threshold)
sander2 marked this conversation as resolved.
Show resolved Hide resolved
}

/// check if the vault is below the liquidation threshold.
Expand Down Expand Up @@ -1597,13 +1683,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 Expand Up @@ -1886,6 +1975,11 @@ impl<T: Config> Pallet<T> {
collateral.convert_to(wrapped_currency)?.checked_div(&threshold)
}

pub fn vault_to_be_backed_tokens(vault_id: &DefaultVaultId<T>) -> Result<Amount<T>, DispatchError> {
nakul1010 marked this conversation as resolved.
Show resolved Hide resolved
let vault = Self::get_active_rich_vault_from_id(vault_id)?;
vault.to_be_backed_tokens()
}

pub fn new_vault_deposit_address(
vault_id: &DefaultVaultId<T>,
secure_id: H256,
Expand Down
Loading