From 831b6fccf16897950276bae9083e4af0377b0f2c Mon Sep 17 00:00:00 2001 From: Sander Bosma Date: Wed, 4 May 2022 14:17:44 +0200 Subject: [PATCH] fix: only award redeem premium upto the secure threshold --- crates/redeem/src/ext.rs | 12 ++++ crates/redeem/src/lib.rs | 30 +++++++--- crates/vault-registry/src/lib.rs | 18 +++++- crates/vault-registry/src/types.rs | 12 ++++ standalone/runtime/tests/mock/mod.rs | 10 ++++ standalone/runtime/tests/test_redeem.rs | 78 +++++++++++++++++++++++++ 6 files changed, 150 insertions(+), 10 deletions(-) diff --git a/crates/redeem/src/ext.rs b/crates/redeem/src/ext.rs index 245f8a04d0..ca73733ba5 100644 --- a/crates/redeem/src/ext.rs +++ b/crates/redeem/src/ext.rs @@ -79,6 +79,12 @@ 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 try_increase_to_be_redeemed_tokens( vault_id: &DefaultVaultId, amount: &Amount, @@ -138,6 +144,12 @@ pub(crate) mod vault_registry { >::is_vault_below_secure_threshold(vault_id) } + pub fn vault_capacity_at_secure_threshold( + vault_id: &DefaultVaultId, + ) -> Result, DispatchError> { + >::vault_capacity_at_secure_threshold(vault_id) + } + pub fn decrease_to_be_redeemed_tokens( vault_id: &DefaultVaultId, tokens: &Amount, diff --git a/crates/redeem/src/lib.rs b/crates/redeem/src/lib.rs index 7d893be4cd..994ebcaa10 100644 --- a/crates/redeem/src/lib.rs +++ b/crates/redeem/src/lib.rs @@ -378,23 +378,35 @@ impl Pallet { Error::::AmountBelowDustAmount ); - // vault will get rid of the btc + btc_inclusion_fee - ext::vault_registry::try_increase_to_be_redeemed_tokens::(&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::(&redeemer); - let below_premium_redeem = ext::vault_registry::is_vault_below_premium_threshold::(&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::(&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::(&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::(&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::(&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 diff --git a/crates/vault-registry/src/lib.rs b/crates/vault-registry/src/lib.rs index 578578d584..6e36d5e39f 100644 --- a/crates/vault-registry/src/lib.rs +++ b/crates/vault-registry/src/lib.rs @@ -1512,8 +1512,11 @@ impl Pallet { } pub fn is_vault_below_premium_threshold(vault_id: &DefaultVaultId) -> Result { + let vault = Self::get_rich_vault_from_id(&vault_id)?; let threshold = Self::premium_redeem_threshold(&vault_id.currencies).ok_or(Error::::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. @@ -1895,6 +1898,19 @@ impl Pallet { collateral.convert_to(wrapped_currency)?.checked_div(&threshold) } + pub fn vault_capacity_at_secure_threshold(vault_id: &DefaultVaultId) -> Result, DispatchError> { + let threshold = Self::secure_collateral_threshold(&vault_id.currencies).ok_or(Error::::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) -> Result, 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, btc_address: BtcAddress) -> DispatchResult { ensure!( !ReservedAddresses::::contains_key(&btc_address), diff --git a/crates/vault-registry/src/types.rs b/crates/vault-registry/src/types.rs index ca1f5b1d4f..ac63a9bfdf 100644 --- a/crates/vault-registry/src/types.rs +++ b/crates/vault-registry/src/types.rs @@ -578,6 +578,18 @@ impl RichVault { 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, DispatchError> { + let amount = self + .data + .issued_tokens + .checked_add(&self.data.to_be_issued_tokens) + .ok_or(Error::::ArithmeticOverflow)? + .checked_sub(&self.data.to_be_redeemed_tokens) + .ok_or(Error::::ArithmeticUnderflow)?; + Ok(Amount::new(amount, self.wrapped_currency())) + } + pub(crate) fn to_be_replaced_tokens(&self) -> Amount { Amount::new(self.data.to_be_replaced_tokens, self.wrapped_currency()) } diff --git a/standalone/runtime/tests/mock/mod.rs b/standalone/runtime/tests/mock/mod.rs index a4b15809ad..a7f8eee452 100644 --- a/standalone/runtime/tests/mock/mod.rs +++ b/standalone/runtime/tests/mock/mod.rs @@ -334,6 +334,16 @@ impl Wrapped for VaultId { } } +pub trait Collateral { + fn collateral(&self, amount: Balance) -> Amount; +} + +impl Collateral for VaultId { + fn collateral(&self, amount: Balance) -> Amount { + Amount::new(amount, self.collateral_currency()) + } +} + pub fn iter_currency_pairs() -> impl Iterator> { iter_collateral_currencies().flat_map(|collateral_id| { iter_wrapped_currencies().map(move |wrapped_id| VaultCurrencyPair { diff --git a/standalone/runtime/tests/test_redeem.rs b/standalone/runtime/tests/test_redeem.rs index 4b9d1073aa..541bb9a228 100644 --- a/standalone/runtime/tests/test_redeem.rs +++ b/standalone/runtime/tests/test_redeem.rs @@ -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)),