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

feature(kensetsu): accrue spamming protection #976

Merged
merged 15 commits into from
Apr 17, 2024
Merged
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
168 changes: 86 additions & 82 deletions pallets/kensetsu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ const VALIDATION_ERROR_LIQUIDATION_LIMIT: u8 = 5;
)]
pub enum CdpType {
/// Pays stability fee in underlying collateral, cannot be liquidated.
V1,
Type1,
/// Pays stability fee in stable coins, can be liquidated.
V2,
Type2,
}

/// Risk management parameters for the specific collateral type.
Expand Down Expand Up @@ -155,11 +155,11 @@ pub mod pallet {
use frame_system::offchain::{SendTransactionTypes, SubmitTransaction};
use frame_system::pallet_prelude::*;
use pallet_timestamp as timestamp;
use sp_arithmetic::traits::{CheckedDiv, CheckedMul, CheckedSub, One, Saturating, Zero};
use sp_arithmetic::traits::{CheckedDiv, CheckedMul, CheckedSub};
use sp_arithmetic::Percent;
use sp_core::bounded::{BoundedBTreeSet, BoundedVec};
use sp_runtime::traits::CheckedConversion;
use sp_std::collections::{btree_set::BTreeSet, vec_deque::VecDeque};
use sp_runtime::traits::{CheckedConversion, One, Zero};
use sp_std::collections::vec_deque::VecDeque;
use sp_std::vec::Vec;

/// CDP id type
Expand Down Expand Up @@ -189,19 +189,9 @@ pub mod pallet {
"Entering off-chain worker, block number is {:?}",
block_number
);
let now = Timestamp::<T>::get();
let outdated_timestamp = now.saturating_sub(T::AccrueInterestPeriod::get());
let mut collaterals_to_update = BTreeSet::new();
for (collateral_asset_id, collateral_info) in CollateralInfos::<T>::iter() {
if collateral_info.last_fee_update_time <= outdated_timestamp {
collaterals_to_update.insert(collateral_asset_id);
}
}
let mut unsafe_cdp_ids = VecDeque::<CdpId>::new();
// TODO optimize CDP accrue (https://github.com/sora-xor/sora2-network/issues/878)
for (cdp_id, cdp) in CDPDepository::<T>::iter() {
// Debt recalculation with interest
if collaterals_to_update.contains(&cdp.collateral_asset_id) {
for (cdp_id, cdp) in <CDPDepository<T>>::iter() {
if let Ok(true) = Self::is_accruable(&cdp_id) {
debug!("Accrue for CDP {:?}", cdp_id);
let call = Call::<T>::accrue { cdp_id };
if let Err(err) =
Expand Down Expand Up @@ -316,14 +306,14 @@ pub mod pallet {
#[pallet::constant]
type MaxRiskManagementTeamSize: Get<u32>;

/// Accrue() for a single CDP can be called once per this period
#[pallet::constant]
type AccrueInterestPeriod: Get<Self::Moment>;

/// A configuration for base priority of unsigned transactions.
#[pallet::constant]
type UnsignedPriority: Get<TransactionPriority>;

/// Minimal uncollected fee in KUSD that triggers offchain worker to call accrue.
#[pallet::constant]
type MinimalStabilityFeeAccrue: Get<Balance>;

/// A configuration for longevity of unsigned transactions.
#[pallet::constant]
type UnsignedLongevity: Get<u64>;
Expand Down Expand Up @@ -378,7 +368,7 @@ pub mod pallet {
#[pallet::storage]
pub type NextCDPId<T> = StorageValue<_, CdpId, ValueQuery>;

/// Storage of all CDPs, where key is an unique CDP identifier
/// Storage of all CDPs, where key is a unique CDP identifier
#[pallet::storage]
#[pallet::getter(fn cdp)]
pub type CDPDepository<T: Config> =
Expand Down Expand Up @@ -486,12 +476,9 @@ pub mod pallet {
/// Risk management team size exceeded
TooManyManagers,
OperationNotPermitted,
NoDebt,
/// Too many CDPs per user
CDPsPerUserLimitReached,
/// Uncollected stability fee is too small for accrue
UncollectedStabilityFeeTooSmall,
HardCapSupply,
BalanceNotEnough,
WrongCollateralAssetId,
AccrueWrongTime,
/// Liquidation lot set in risk parameters is zero, cannot liquidate
ZeroLiquidationLot,
Expand Down Expand Up @@ -553,7 +540,7 @@ pub mod pallet {
owner: who.clone(),
collateral_asset_id,
debt_asset_id: T::KusdAssetId::get(),
cdp_type: CdpType::V2,
cdp_type: CdpType::Type2,
});
if collateral_amount > 0 {
Self::deposit_internal(&who, cdp_id, collateral_amount)?;
Expand Down Expand Up @@ -729,7 +716,10 @@ pub mod pallet {
#[pallet::call_index(6)]
#[pallet::weight(<T as Config>::WeightInfo::accrue())]
pub fn accrue(_origin: OriginFor<T>, cdp_id: CdpId) -> DispatchResult {
ensure!(Self::is_accruable(&cdp_id)?, Error::<T>::NoDebt);
ensure!(
Self::is_accruable(&cdp_id)?,
Error::<T>::UncollectedStabilityFeeTooSmall
);
Self::accrue_internal(cdp_id)?;
Ok(())
}
Expand Down Expand Up @@ -925,15 +915,12 @@ pub mod pallet {
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;

/// It is allowed to call only accrue() and liquidate() and only if
/// it fulfills conditions.
/// It is allowed to call accrue() and liquidate() only if it fulfills conditions.
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
if !Self::check_liquidation_available() {
return InvalidTransaction::Custom(VALIDATION_ERROR_LIQUIDATION_LIMIT).into();
}
match call {
// TODO spamming with accrue calls, add some filter to not call too often
// https://github.com/sora-xor/sora2-network/issues/878
Call::accrue { cdp_id } => {
if Self::is_accruable(cdp_id)
.map_err(|_| InvalidTransaction::Custom(VALIDATION_ERROR_ACCRUE))?
Expand Down Expand Up @@ -977,7 +964,7 @@ pub mod pallet {
}

impl<T: Config> Pallet<T> {
/// Ensures that `who` is a risk manager
/// Ensures that `who` is a risk manager.
/// Risk manager can set protocol risk parameters.
fn ensure_risk_manager(who: &AccountIdOf<T>) -> DispatchResult {
if !Self::risk_managers().map_or(false, |risk_managers| risk_managers.contains(who)) {
Expand Down Expand Up @@ -1218,7 +1205,7 @@ pub mod pallet {
Ok(())
}

/// Repays debt
/// Repays debt.
/// Burns KUSD amount from CDP owner, updates CDP balances.
///
/// ## Parameters
Expand Down Expand Up @@ -1281,49 +1268,76 @@ pub mod pallet {
Ok(())
}

/// Returns true if CDP has debt.
/// Returns true if CDP has debt and uncollected stability fee is more than threshold.
fn is_accruable(cdp_id: &CdpId) -> Result<bool, DispatchError> {
let cdp = Self::cdp(cdp_id).ok_or(Error::<T>::CDPNotFound)?;
Ok(cdp.debt > 0)
if cdp.debt > 0 {
let uncollected_stability_fee = Self::calculate_stability_fee(*cdp_id)?;
Ok(uncollected_stability_fee >= T::MinimalStabilityFeeAccrue::get())
} else {
Ok(false)
}
}

/// Recalculates collateral interest coefficient with the current timestamp
fn update_collateral_interest_coefficient(
///
/// Note:
/// In the case of update this code do not forget to update front-end logic:
/// `sora2-substrate-js-library/packages/util/src/kensetsu/index.ts`
/// function `updateCollateralInterestCoefficient`
fn calculate_collateral_interest_coefficient(
collateral_asset_id: &AssetIdOf<T>,
) -> Result<CollateralInfo<T::Moment>, DispatchError> {
let collateral_info =
CollateralInfos::<T>::try_mutate(collateral_asset_id, |collateral_info| {
let collateral_info = collateral_info
.as_mut()
.ok_or(Error::<T>::CollateralInfoNotFound)?;
let now = Timestamp::<T>::get();
ensure!(
now >= collateral_info.last_fee_update_time,
Error::<T>::AccrueWrongTime
);
// do not update if time is the same
if now > collateral_info.last_fee_update_time {
let time_passed = now
.checked_sub(&collateral_info.last_fee_update_time)
.ok_or(Error::<T>::ArithmeticError)?;
let new_coefficient = compound(
collateral_info.interest_coefficient.into_inner(),
collateral_info.risk_parameters.stability_fee_rate,
time_passed
.checked_into::<u64>()
.ok_or(Error::<T>::ArithmeticError)?,
)
.map_err(|_| Error::<T>::ArithmeticError)?;
collateral_info.last_fee_update_time = now;
collateral_info.interest_coefficient =
FixedU128::from_inner(new_coefficient);
}
Ok::<CollateralInfo<T::Moment>, DispatchError>(collateral_info.clone())
})?;
let mut collateral_info = CollateralInfos::<T>::get(collateral_asset_id)
.ok_or(Error::<T>::CollateralInfoNotFound)?;
let now = Timestamp::<T>::get();
ensure!(
now >= collateral_info.last_fee_update_time,
Error::<T>::AccrueWrongTime
);

// do not update if time is the same
if now > collateral_info.last_fee_update_time {
let time_passed = now
.checked_sub(&collateral_info.last_fee_update_time)
.ok_or(Error::<T>::ArithmeticError)?;
let new_coefficient = compound(
collateral_info.interest_coefficient.into_inner(),
collateral_info.risk_parameters.stability_fee_rate,
time_passed
.checked_into::<u64>()
.ok_or(Error::<T>::ArithmeticError)?,
)
.map_err(|_| Error::<T>::ArithmeticError)?;
collateral_info.last_fee_update_time = now;
collateral_info.interest_coefficient = FixedU128::from_inner(new_coefficient);
}
Ok(collateral_info)
}

/// Calculates stability fee for the CDP for the current time.
///
/// Note:
/// In the case of update this code do not forget to update front-end logic:
/// `sora2-substrate-js-library/packages/util/src/kensetsu/index.ts`
/// function `calcNewDebt`
fn calculate_stability_fee(cdp_id: CdpId) -> Result<Balance, DispatchError> {
let cdp = Self::cdp(cdp_id).ok_or(Error::<T>::CDPNotFound)?;
let collateral_info =
Self::calculate_collateral_interest_coefficient(&cdp.collateral_asset_id)?;
let interest_coefficient = collateral_info.interest_coefficient;
let interest_percent = interest_coefficient
.checked_sub(&cdp.interest_coefficient)
.ok_or(Error::<T>::ArithmeticError)?
.checked_div(&cdp.interest_coefficient)
.ok_or(Error::<T>::ArithmeticError)?;
wer1st marked this conversation as resolved.
Show resolved Hide resolved
let stability_fee = FixedU128::from_inner(cdp.debt)
.checked_mul(&interest_percent)
.ok_or(Error::<T>::ArithmeticError)?
.into_inner();
Ok(stability_fee)
}

/// Accrues interest on a Collateralized Debt Position (CDP) and updates relevant parameters.
///
/// ## Parameters
Expand All @@ -1335,17 +1349,9 @@ pub mod pallet {
{
let mut cdp = Self::cdp(cdp_id).ok_or(Error::<T>::CDPNotFound)?;
let collateral_info =
Self::update_collateral_interest_coefficient(&cdp.collateral_asset_id)?;
Self::calculate_collateral_interest_coefficient(&cdp.collateral_asset_id)?;
let new_coefficient = collateral_info.interest_coefficient;
let interest_percent = (new_coefficient
.checked_sub(&cdp.interest_coefficient)
.ok_or(Error::<T>::ArithmeticError)?)
.checked_div(&cdp.interest_coefficient)
.ok_or(Error::<T>::ArithmeticError)?;
let mut stability_fee = FixedU128::from_inner(cdp.debt)
.checked_mul(&interest_percent)
.ok_or(Error::<T>::ArithmeticError)?
.into_inner();
let mut stability_fee = Self::calculate_stability_fee(cdp_id)?;
let new_debt = cdp
.debt
.checked_add(stability_fee)
Expand Down Expand Up @@ -1577,9 +1583,7 @@ pub mod pallet {
/// Increments CDP Id counter, changes storage state.
fn increment_cdp_id() -> Result<CdpId, DispatchError> {
NextCDPId::<T>::try_mutate(|cdp_id| {
*cdp_id = cdp_id
.checked_add(1)
.ok_or(crate::pallet::Error::<T>::ArithmeticError)?;
*cdp_id = cdp_id.checked_add(1).ok_or(Error::<T>::ArithmeticError)?;
Ok(*cdp_id)
})
}
Expand Down Expand Up @@ -1607,7 +1611,7 @@ pub mod pallet {

/// Updates CDP debt balance
fn update_cdp_debt(cdp_id: CdpId, debt: Balance) -> DispatchResult {
crate::pallet::CDPDepository::<T>::try_mutate(cdp_id, |cdp| {
CDPDepository::<T>::try_mutate(cdp_id, |cdp| {
let cdp = cdp.as_mut().ok_or(Error::<T>::CDPNotFound)?;
cdp.debt = debt;
Ok(())
Expand Down Expand Up @@ -1684,7 +1688,7 @@ pub mod pallet {
match option_collateral_info {
Some(collateral_info) => {
let mut new_info =
Self::update_collateral_interest_coefficient(collateral_asset_id)?;
Self::calculate_collateral_interest_coefficient(collateral_asset_id)?;
new_info.risk_parameters = new_risk_parameters;
*collateral_info = new_info;
}
Expand Down
8 changes: 2 additions & 6 deletions pallets/kensetsu/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,8 @@ parameter_types! {
};
pub const KenAssetId: AssetId = KEN;
pub const KusdAssetId: AssetId = KUSD;

pub const GetKenIncentiveRemintPercent: Percent = Percent::from_percent(80);

// 1 day
pub const AccrueInterestPeriod: Moment = 86_400_000;

pub const MinimalStabilityFeeAccrue: Balance = balance!(1);
}

mock_assets_config!(TestRuntime);
Expand All @@ -252,7 +248,7 @@ impl kensetsu::Config for TestRuntime {
type KenIncentiveRemintPercent = GetKenIncentiveRemintPercent;
type MaxCdpsPerOwner = ConstU32<100>;
type MaxRiskManagementTeamSize = ConstU32<100>;
type AccrueInterestPeriod = AccrueInterestPeriod;
type MinimalStabilityFeeAccrue = MinimalStabilityFeeAccrue;
type UnsignedPriority = ConstU64<100>;
type UnsignedLongevity = ConstU64<100>;
type WeightInfo = ();
Expand Down
27 changes: 12 additions & 15 deletions pallets/kensetsu/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use frame_support::assert_ok;
use frame_system::pallet_prelude::OriginFor;
use hex_literal::hex;
use sp_arithmetic::{Perbill, Percent};
use sp_runtime::traits::{One, Zero};
use sp_runtime::traits::Zero;
use sp_runtime::AccountId32;

type AccountId = AccountId32;
Expand Down Expand Up @@ -123,21 +123,18 @@ pub fn set_xor_as_collateral_type(
stability_fee_rate: FixedU128,
minimal_collateral_deposit: Balance,
) {
CollateralInfos::<TestRuntime>::set(
set_up_risk_manager();
assert_ok!(KensetsuPallet::update_collateral_risk_parameters(
risk_manager(),
XOR,
Some(CollateralInfo {
risk_parameters: CollateralRiskParameters {
hard_cap,
max_liquidation_lot: balance!(1000),
liquidation_ratio,
stability_fee_rate,
minimal_collateral_deposit,
},
kusd_supply: balance!(0),
last_fee_update_time: 0,
interest_coefficient: FixedU128::one(),
}),
);
CollateralRiskParameters {
hard_cap,
max_liquidation_lot: balance!(1000),
liquidation_ratio,
stability_fee_rate,
minimal_collateral_deposit,
}
));
KusdHardCap::<TestRuntime>::set(hard_cap);
}

Expand Down
Loading
Loading