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 #589

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions crates/redeem/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ 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 try_increase_to_be_redeemed_tokens<T: crate::Config>(
vault_id: &DefaultVaultId<T>,
amount: &Amount<T>,
Expand Down Expand Up @@ -138,6 +144,12 @@ pub(crate) mod vault_registry {
<vault_registry::Pallet<T>>::is_vault_below_secure_threshold(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 decrease_to_be_redeemed_tokens<T: crate::Config>(
vault_id: &DefaultVaultId<T>,
tokens: &Amount<T>,
Expand Down
30 changes: 21 additions & 9 deletions crates/redeem/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,23 +378,35 @@ 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)?
// we only award a premium on the amount ok tokens required to bring
// `issued + to_be_issued - to_be_redeemed` back to the secure threshold

let 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)?;

// the amount of tokens that we can give a premium for
let max_premium_tokens = to_be_backed_tokens.saturating_sub(&capacity)?;
// the actual amount of tokens redeemed that we give a premium for
let actual_premium_tokens = max_premium_tokens.min(&user_to_be_received_btc)?;
// converted to collateral..
let premium_tokens_in_collateral = actual_premium_tokens.convert_to(currency_id)?;

ext::fee::get_premium_redeem_fee::<T>(&premium_tokens_in_collateral)?
} 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)?;

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

// decrease to-be-replaced tokens - when the vault requests tokens to be replaced, it
// want to get rid of tokens, and it does not matter whether this is through a redeem,
// or a replace. As such, we decrease the to-be-replaced tokens here. This call will
Expand Down
18 changes: 17 additions & 1 deletion crates/vault-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1512,8 +1512,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)
}

/// check if the vault is below the liquidation threshold.
Expand Down Expand Up @@ -1895,6 +1898,19 @@ impl<T: Config> Pallet<T> {
collateral.convert_to(wrapped_currency)?.checked_div(&threshold)
}

pub fn vault_capacity_at_secure_threshold(vault_id: &DefaultVaultId<T>) -> Result<Amount<T>, DispatchError> {
let threshold = Self::secure_collateral_threshold(&vault_id.currencies).ok_or(Error::<T>::ThresholdNotSet)?;
let collateral = Self::get_backing_collateral(vault_id)?;
let wrapped_currency = vault_id.wrapped_currency();

Self::calculate_max_wrapped_from_collateral_for_threshold(&collateral, wrapped_currency, threshold)
}

pub fn vault_to_be_backed_tokens(vault_id: &DefaultVaultId<T>) -> Result<Amount<T>, DispatchError> {
let vault = Self::get_active_rich_vault_from_id(vault_id)?;
vault.to_be_backed_tokens()
}

pub fn insert_vault_deposit_address(vault_id: DefaultVaultId<T>, btc_address: BtcAddress) -> DispatchResult {
ensure!(
!ReservedAddresses::<T>::contains_key(&btc_address),
Expand Down
12 changes: 12 additions & 0 deletions crates/vault-registry/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,18 @@ impl<T: Config> RichVault<T> {
Ok(Amount::new(amount, self.wrapped_currency()))
}

/// the number of issued tokens if all issues and redeems execute successfully
pub(crate) fn to_be_backed_tokens(&self) -> Result<Amount<T>, DispatchError> {
let amount = self
.data
.issued_tokens
.checked_add(&self.data.to_be_issued_tokens)
.ok_or(Error::<T>::ArithmeticOverflow)?
.checked_sub(&self.data.to_be_redeemed_tokens)
.ok_or(Error::<T>::ArithmeticUnderflow)?;
Ok(Amount::new(amount, self.wrapped_currency()))
}

pub(crate) fn to_be_replaced_tokens(&self) -> Amount<T> {
Amount::new(self.data.to_be_replaced_tokens, self.wrapped_currency())
}
Expand Down
10 changes: 10 additions & 0 deletions standalone/runtime/tests/mock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,16 @@ impl Wrapped for VaultId {
}
}

pub trait Collateral {
fn collateral(&self, amount: Balance) -> Amount<Runtime>;
}

impl Collateral for VaultId {
fn collateral(&self, amount: Balance) -> Amount<Runtime> {
Amount::new(amount, self.collateral_currency())
}
}

pub fn iter_currency_pairs() -> impl Iterator<Item = DefaultVaultCurrencyPair<Runtime>> {
iter_collateral_currencies().flat_map(|collateral_id| {
iter_wrapped_currencies().map(move |wrapped_id| VaultCurrencyPair {
Expand Down
78 changes: 78 additions & 0 deletions standalone/runtime/tests/test_redeem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1910,6 +1910,84 @@ fn integration_test_redeem_wrapped_execute_liquidated() {
});
}

mod premium_redeem_tests {
use super::{assert_eq, *};

fn setup_vault_below_secure_threshold(vault_id: VaultId) {
let secure = FixedU128::checked_from_rational(200, 100).unwrap();
let premium = FixedU128::checked_from_rational(160, 100).unwrap();
VaultRegistryPallet::_set_secure_collateral_threshold(vault_id.currencies.clone(), secure);
VaultRegistryPallet::_set_premium_redeem_threshold(vault_id.currencies.clone(), premium);

assert_ok!(OraclePallet::_set_exchange_rate(
vault_id.collateral_currency(),
FixedU128::from(2)
));

// with 2000 collateral and exchange rate at 2, the vault is at:
// - secure threshold (200%) when it has 2000/2/2 = 500 tokens
// - premium threshold (160%) when it has 2000/2/1.6 = 625 tokens

// we award premium redeem only for the amount needed for (issued + to_be_issued - to_be_redeemed)
// to reach the secure threshold

// setup the vault such that (issued + to_be_issued - to_be_redeemed) = (450 + 250 - 50) = 650
// (everything scaled by 1000 to prevent getting dust amount errors)
CoreVaultData::force_to(
&vault_id,
CoreVaultData {
issued: vault_id.wrapped(450_000),
to_be_issued: vault_id.wrapped(250_000),
to_be_redeemed: vault_id.wrapped(50_000),
backing_collateral: vault_id.collateral(2_000_000),
to_be_replaced: vault_id.wrapped(0),
replace_collateral: griefing(0),
..default_vault_state(&vault_id)
},
);
}

#[test]
fn integration_test_premium_redeem_with_reward_for_only_part_of_the_request() {
test_with(|vault_id| {
setup_vault_below_secure_threshold(vault_id.clone());

let redeem_id = setup_redeem(vault_id.wrapped(400_000), USER, &vault_id);
let redeem = RedeemPallet::get_open_redeem_request_from_id(&redeem_id).unwrap();

// we should get rewarded only for 150_000 tokens (that's when we reach secure threshold)
let expected_premium = FeePallet::get_premium_redeem_fee(
&vault_id
.wrapped(150_000)
.convert_to(vault_id.collateral_currency())
.unwrap(),
)
.unwrap();
assert_eq!(vault_id.collateral(redeem.premium), expected_premium);
});
}

#[test]
fn integration_test_premium_redeem_with_reward_for_full_request() {
test_with(|vault_id| {
setup_vault_below_secure_threshold(vault_id.clone());

let redeem_id = setup_redeem(vault_id.wrapped(100_000), USER, &vault_id);
let redeem = RedeemPallet::get_open_redeem_request_from_id(&redeem_id).unwrap();

// we should get rewarded for the full amount, since we did not reach secure threshold
let expected_premium = FeePallet::get_premium_redeem_fee(
&vault_id
.wrapped(redeem.amount_btc)
.convert_to(vault_id.collateral_currency())
.unwrap(),
)
.unwrap();
assert_eq!(vault_id.collateral(redeem.premium), expected_premium);
});
}
}

fn get_additional_collateral(vault_id: &VaultId) {
assert_ok!(VaultRegistryPallet::transfer_funds(
CurrencySource::FreeBalance(account_of(FAUCET)),
Expand Down