diff --git a/Cargo.lock b/Cargo.lock index 8e41747e2f..958edbc381 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4641,6 +4641,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + [[package]] name = "jsonrpsee" version = "0.16.2" @@ -5715,6 +5721,7 @@ dependencies = [ "frame-system", "interbtc-primitives", "mocktopus", + "mutagen", "num-traits", "oracle", "orml-oracle", @@ -6185,6 +6192,39 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "mutagen" +version = "0.2.0" +source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" +dependencies = [ + "mutagen-core", + "mutagen-transform", +] + +[[package]] +name = "mutagen-core" +version = "0.2.0" +source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" +dependencies = [ + "anyhow", + "json", + "lazy_static", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "mutagen-transform" +version = "0.2.0" +source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" +dependencies = [ + "mutagen-core", + "proc-macro2", +] + [[package]] name = "nalgebra" version = "0.32.2" diff --git a/crates/loans/Cargo.toml b/crates/loans/Cargo.toml index 288434c9d9..b51283ca50 100644 --- a/crates/loans/Cargo.toml +++ b/crates/loans/Cargo.toml @@ -45,6 +45,7 @@ orml-oracle = { git = "https://github.com/open-web3-stack/open-runtime-module-li mocktopus = "0.8.0" visibility = { version = "0.0.1" } currency = { path = "../currency", features = ["testing-utils"] } +mutagen = { git = "https://github.com/llogiq/mutagen" } pallet-scheduler = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31" } [features] diff --git a/crates/loans/src/farming.rs b/crates/loans/src/farming.rs index 59225496b1..1712f61c4f 100644 --- a/crates/loans/src/farming.rs +++ b/crates/loans/src/farming.rs @@ -24,10 +24,12 @@ impl Pallet { T::PalletId::get().into_sub_account_truncating(REWARD_SUB_ACCOUNT) } + #[cfg_attr(test, mutate)] fn reward_scale() -> u128 { 10_u128.pow(12) } + #[cfg_attr(test, mutate)] fn calculate_reward_delta_index( delta_block: T::BlockNumber, reward_speed: BalanceOf, @@ -47,6 +49,7 @@ impl Pallet { Ok(delta_index) } + #[cfg_attr(test, mutate)] fn calculate_reward_delta( share: BalanceOf, reward_delta_index: u128, @@ -60,6 +63,7 @@ impl Pallet { Ok(reward_delta) } + #[cfg_attr(test, mutate)] pub(crate) fn update_reward_supply_index(asset_id: CurrencyId) -> DispatchResult { let current_block_number = >::block_number(); RewardSupplyState::::try_mutate(asset_id, |supply_state| -> DispatchResult { @@ -82,6 +86,7 @@ impl Pallet { }) } + #[cfg_attr(test, mutate)] pub(crate) fn update_reward_borrow_index(asset_id: CurrencyId) -> DispatchResult { let current_block_number = >::block_number(); RewardBorrowState::::try_mutate(asset_id, |borrow_state| -> DispatchResult { @@ -106,6 +111,7 @@ impl Pallet { }) } + #[cfg_attr(test, mutate)] pub(crate) fn distribute_supplier_reward(asset_id: CurrencyId, supplier: &T::AccountId) -> DispatchResult { RewardSupplierIndex::::try_mutate(asset_id, supplier, |supplier_index| -> DispatchResult { let supply_state = RewardSupplyState::::get(asset_id); @@ -137,6 +143,7 @@ impl Pallet { }) } + #[cfg_attr(test, mutate)] pub(crate) fn distribute_borrower_reward(asset_id: CurrencyId, borrower: &T::AccountId) -> DispatchResult { RewardBorrowerIndex::::try_mutate(asset_id, borrower, |borrower_index| -> DispatchResult { let borrow_state = RewardBorrowState::::get(asset_id); @@ -166,6 +173,7 @@ impl Pallet { }) } + #[cfg_attr(test, mutate)] pub(crate) fn collect_market_reward(asset_id: CurrencyId, user: &T::AccountId) -> DispatchResult { Self::update_reward_supply_index(asset_id)?; Self::distribute_supplier_reward(asset_id, user)?; @@ -176,6 +184,7 @@ impl Pallet { Ok(()) } + #[cfg_attr(test, mutate)] pub(crate) fn pay_reward(user: &T::AccountId) -> DispatchResult { let pool_account = Self::reward_account_id(); let reward_asset = T::RewardAssetId::get(); diff --git a/crates/loans/src/interest.rs b/crates/loans/src/interest.rs index 095113c14d..48c5f7c0b8 100644 --- a/crates/loans/src/interest.rs +++ b/crates/loans/src/interest.rs @@ -23,6 +23,7 @@ use crate::*; impl Pallet { /// Accrue interest and update corresponding storage #[cfg_attr(any(test, feature = "integration-tests"), visibility::make(pub))] + #[cfg_attr(test, mutate)] pub(crate) fn accrue_interest(asset_id: CurrencyId) -> DispatchResult { let now = T::UnixTime::now().as_secs(); let last_accrued_interest_time = Self::last_accrued_interest_time(asset_id); @@ -64,6 +65,7 @@ impl Pallet { Ok(()) } + #[cfg_attr(test, mutate)] pub fn get_market_status( asset_id: CurrencyId, ) -> Result<(Rate, Rate, Rate, Ratio, BalanceOf, BalanceOf, FixedU128), DispatchError> { @@ -124,6 +126,7 @@ impl Pallet { /// Update the exchange rate according to the totalCash, totalBorrows and totalSupply. /// This function does not accrue interest before calculating the exchange rate. /// exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply + #[cfg_attr(test, mutate)] pub fn exchange_rate_stored(asset_id: CurrencyId) -> Result { let total_supply = Self::total_supply(asset_id)?; let total_cash = Self::get_total_cash(asset_id); @@ -136,6 +139,7 @@ impl Pallet { /// Calculate the borrowing utilization ratio of the specified market /// /// utilizationRatio = totalBorrows / (totalCash + totalBorrows − totalReserves) + #[cfg_attr(test, mutate)] pub(crate) fn calc_utilization_ratio( cash: &Amount, borrows: &Amount, @@ -155,6 +159,7 @@ impl Pallet { /// This ensures the exchange rate cannot be attacked by a deposit so big that /// subsequent deposits to receive zero lendTokens (because of rounding down). See this /// PR for more details: https://github.com/parallel-finance/parallel/pull/1552/files + #[cfg_attr(test, mutate)] pub(crate) fn ensure_valid_exchange_rate(exchange_rate: Rate) -> DispatchResult { ensure!( exchange_rate >= Self::min_exchange_rate() && exchange_rate < Self::max_exchange_rate(), @@ -164,6 +169,7 @@ impl Pallet { Ok(()) } + #[cfg_attr(test, mutate)] pub(crate) fn update_last_accrued_interest_time(asset_id: CurrencyId, time: Timestamp) -> DispatchResult { LastAccruedInterestTime::::try_mutate(asset_id, |last_time| -> DispatchResult { *last_time = time; @@ -171,6 +177,7 @@ impl Pallet { }) } + #[cfg_attr(test, mutate)] pub(crate) fn accrue_index(borrow_rate: Rate, index: Rate, delta_time: Timestamp) -> Result { // Compound interest: // new_index = old_index * (1 + annual_borrow_rate / SECONDS_PER_YEAR) ^ delta_time @@ -184,6 +191,7 @@ impl Pallet { Ok(index.checked_mul(&compounded_rate).ok_or(ArithmeticError::Overflow)?) } + #[cfg_attr(test, mutate)] fn calculate_exchange_rate( total_supply: &Amount, total_cash: &Amount, diff --git a/crates/loans/src/lib.rs b/crates/loans/src/lib.rs index e5099356c9..cd422f5880 100644 --- a/crates/loans/src/lib.rs +++ b/crates/loans/src/lib.rs @@ -59,6 +59,9 @@ pub use default_weights::WeightInfo; pub use orml_traits::currency::{OnDeposit, OnSlash, OnTransfer}; pub use types::{BorrowSnapshot, EarnedSnapshot, Market, MarketState, RewardMarketState}; +#[cfg(test)] +use mutagen::mutate; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -121,6 +124,7 @@ pub struct OnSlashHook(marker::PhantomData); impl OnSlash, BalanceOf> for OnSlashHook { /// Whenever a lend_token balance is mutated, the supplier incentive rewards accumulated up to that point /// have to be distributed. + #[cfg_attr(test, mutate)] fn on_slash(currency_id: CurrencyId, account_id: &T::AccountId, amount: BalanceOf) { if currency_id.is_lend_token() { // Note that wherever `on_slash` is called in the lending pallet, and the `account_id` has non-zero @@ -150,6 +154,7 @@ pub struct PreDeposit(marker::PhantomData); impl OnDeposit, BalanceOf> for PreDeposit { /// Whenever a lend_token balance is mutated, the supplier incentive rewards accumulated up to that point /// have to be distributed. + #[cfg_attr(test, mutate)] fn on_deposit(currency_id: CurrencyId, account_id: &T::AccountId, _amount: BalanceOf) -> DispatchResult { if currency_id.is_lend_token() { let underlying_id = Pallet::::underlying_id(currency_id)?; @@ -162,6 +167,7 @@ impl OnDeposit, BalanceOf> for PreDepo pub struct PostDeposit(marker::PhantomData); impl OnDeposit, BalanceOf> for PostDeposit { + #[cfg_attr(test, mutate)] fn on_deposit(currency_id: CurrencyId, account_id: &T::AccountId, amount: BalanceOf) -> DispatchResult { if currency_id.is_lend_token() { Pallet::::lock_if_account_deposited(account_id, &Amount::new(amount, currency_id))?; @@ -174,6 +180,7 @@ pub struct PreTransfer(marker::PhantomData); impl OnTransfer, BalanceOf> for PreTransfer { /// Whenever a lend_token balance is mutated, the supplier incentive rewards accumulated up to that point /// have to be distributed. + #[cfg_attr(test, mutate)] fn on_transfer( currency_id: CurrencyId, from: &T::AccountId, @@ -195,6 +202,7 @@ impl OnTransfer, BalanceOf> for PostTr /// If an account has locked their lend_token balance as collateral, any incoming lend_tokens /// have to be automatically locked as well, in order to enforce a "collateral toggle" that /// offers the same UX as Compound V2's lending protocol implementation. + #[cfg_attr(test, mutate)] fn on_transfer( currency_id: CurrencyId, _from: &T::AccountId, @@ -613,6 +621,7 @@ pub mod pallet { #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::add_market())] #[transactional] + #[cfg_attr(test, mutate)] pub fn add_market( origin: OriginFor, asset_id: CurrencyId, @@ -682,6 +691,7 @@ pub mod pallet { #[pallet::call_index(1)] #[pallet::weight(::WeightInfo::activate_market())] #[transactional] + #[cfg_attr(test, mutate)] pub fn activate_market(origin: OriginFor, asset_id: CurrencyId) -> DispatchResultWithPostInfo { T::UpdateOrigin::ensure_origin(origin)?; // TODO: if the market is already active throw an error, @@ -707,6 +717,7 @@ pub mod pallet { #[pallet::call_index(2)] #[pallet::weight(::WeightInfo::update_rate_model())] #[transactional] + #[cfg_attr(test, mutate)] pub fn update_rate_model( origin: OriginFor, asset_id: CurrencyId, @@ -740,6 +751,7 @@ pub mod pallet { #[pallet::call_index(3)] #[pallet::weight(::WeightInfo::update_market())] #[transactional] + #[cfg_attr(test, mutate)] pub fn update_market( origin: OriginFor, asset_id: CurrencyId, @@ -816,6 +828,7 @@ pub mod pallet { #[pallet::call_index(4)] #[pallet::weight(::WeightInfo::force_update_market())] #[transactional] + #[cfg_attr(test, mutate)] pub fn force_update_market( origin: OriginFor, asset_id: CurrencyId, @@ -848,6 +861,7 @@ pub mod pallet { #[pallet::call_index(5)] #[pallet::weight(::WeightInfo::add_reward())] #[transactional] + #[cfg_attr(test, mutate)] pub fn add_reward(origin: OriginFor, amount: BalanceOf) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; ensure!(!amount.is_zero(), Error::::InvalidAmount); @@ -873,6 +887,7 @@ pub mod pallet { #[pallet::call_index(6)] #[pallet::weight(::WeightInfo::update_market_reward_speed())] #[transactional] + #[cfg_attr(test, mutate)] pub fn update_market_reward_speed( origin: OriginFor, asset_id: CurrencyId, @@ -916,6 +931,7 @@ pub mod pallet { #[pallet::call_index(7)] #[pallet::weight(::WeightInfo::claim_reward())] #[transactional] + #[cfg_attr(test, mutate)] pub fn claim_reward(origin: OriginFor) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; @@ -934,6 +950,7 @@ pub mod pallet { #[pallet::call_index(8)] #[pallet::weight(::WeightInfo::claim_reward_for_market())] #[transactional] + #[cfg_attr(test, mutate)] pub fn claim_reward_for_market(origin: OriginFor, asset_id: CurrencyId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; @@ -952,6 +969,7 @@ pub mod pallet { #[pallet::call_index(9)] #[pallet::weight(::WeightInfo::mint())] #[transactional] + #[cfg_attr(test, mutate)] pub fn mint( origin: OriginFor, asset_id: CurrencyId, @@ -972,6 +990,7 @@ pub mod pallet { #[pallet::call_index(10)] #[pallet::weight(::WeightInfo::redeem())] #[transactional] + #[cfg_attr(test, mutate)] pub fn redeem( origin: OriginFor, asset_id: CurrencyId, @@ -996,6 +1015,7 @@ pub mod pallet { #[pallet::call_index(11)] #[pallet::weight(::WeightInfo::redeem_all())] #[transactional] + #[cfg_attr(test, mutate)] pub fn redeem_all(origin: OriginFor, asset_id: CurrencyId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin.clone())?; @@ -1017,6 +1037,7 @@ pub mod pallet { #[pallet::call_index(12)] #[pallet::weight(::WeightInfo::borrow())] #[transactional] + #[cfg_attr(test, mutate)] pub fn borrow( origin: OriginFor, asset_id: CurrencyId, @@ -1037,6 +1058,7 @@ pub mod pallet { #[pallet::call_index(13)] #[pallet::weight(::WeightInfo::repay_borrow())] #[transactional] + #[cfg_attr(test, mutate)] pub fn repay_borrow( origin: OriginFor, asset_id: CurrencyId, @@ -1056,6 +1078,7 @@ pub mod pallet { #[pallet::call_index(14)] #[pallet::weight(::WeightInfo::repay_borrow_all())] #[transactional] + #[cfg_attr(test, mutate)] pub fn repay_borrow_all(origin: OriginFor, asset_id: CurrencyId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; Self::ensure_active_market(asset_id)?; @@ -1081,6 +1104,7 @@ pub mod pallet { #[pallet::call_index(15)] #[pallet::weight(::WeightInfo::deposit_all_collateral())] #[transactional] + #[cfg_attr(test, mutate)] pub fn deposit_all_collateral(origin: OriginFor, asset_id: CurrencyId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let free_lend_tokens = Self::free_lend_tokens(asset_id, &who)?; @@ -1103,6 +1127,7 @@ pub mod pallet { #[pallet::call_index(16)] #[pallet::weight(::WeightInfo::withdraw_all_collateral())] #[transactional] + #[cfg_attr(test, mutate)] pub fn withdraw_all_collateral(origin: OriginFor, asset_id: CurrencyId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; @@ -1129,6 +1154,7 @@ pub mod pallet { #[pallet::call_index(17)] #[pallet::weight(::WeightInfo::liquidate_borrow())] #[transactional] + #[cfg_attr(test, mutate)] pub fn liquidate_borrow( origin: OriginFor, borrower: T::AccountId, @@ -1158,6 +1184,7 @@ pub mod pallet { #[pallet::call_index(18)] #[pallet::weight(::WeightInfo::add_reserves())] #[transactional] + #[cfg_attr(test, mutate)] pub fn add_reserves( origin: OriginFor, payer: ::Source, @@ -1196,6 +1223,7 @@ pub mod pallet { #[pallet::call_index(19)] #[pallet::weight(::WeightInfo::reduce_reserves())] #[transactional] + #[cfg_attr(test, mutate)] pub fn reduce_reserves( origin: OriginFor, receiver: ::Source, @@ -1236,6 +1264,7 @@ pub mod pallet { #[pallet::call_index(20)] #[pallet::weight(::WeightInfo::reduce_incentive_reserves())] #[transactional] + #[cfg_attr(test, mutate)] pub fn reduce_incentive_reserves( origin: OriginFor, receiver: ::Source, @@ -1285,6 +1314,7 @@ impl Pallet { T::PalletId::get().into_account_truncating() } + #[cfg_attr(test, mutate)] pub fn get_account_liquidity(account: &T::AccountId) -> Result, DispatchError> { let total_collateral_value = Self::total_collateral_value(account)?; let total_borrow_value = Self::total_borrowed_value(account)?; @@ -1298,6 +1328,7 @@ impl Pallet { AccountLiquidity::from_collateral_and_debt(total_collateral_value, total_borrow_value) } + #[cfg_attr(test, mutate)] pub fn get_account_liquidation_threshold_liquidity( account: &T::AccountId, ) -> Result, DispatchError> { @@ -1313,6 +1344,7 @@ impl Pallet { AccountLiquidity::from_collateral_and_debt(total_collateral_value, total_borrow_value) } + #[cfg_attr(test, mutate)] fn total_borrowed_value(borrower: &T::AccountId) -> Result, DispatchError> { let mut total_borrow_value = Amount::::zero(T::ReferenceAssetId::get()); for (asset_id, _) in Self::active_markets() { @@ -1327,6 +1359,7 @@ impl Pallet { Ok(total_borrow_value) } + #[cfg_attr(test, mutate)] fn collateral_amount_value(voucher: &Amount) -> Result, DispatchError> { let underlying = voucher.to_underlying()?; let market = Self::market(underlying.currency())?; @@ -1335,6 +1368,7 @@ impl Pallet { Self::get_asset_value(&effects) } + #[cfg_attr(test, mutate)] fn collateral_asset_value(supplier: &T::AccountId, asset_id: CurrencyId) -> Result, DispatchError> { let lend_token_id = Self::lend_token_id(asset_id)?; let deposits = Self::account_deposits(lend_token_id, supplier); @@ -1344,6 +1378,7 @@ impl Pallet { Self::collateral_amount_value(&deposits) } + #[cfg_attr(test, mutate)] fn liquidation_threshold_asset_value( borrower: &T::AccountId, asset_id: CurrencyId, @@ -1363,6 +1398,7 @@ impl Pallet { Self::get_asset_value(&effects_amount) } + #[cfg_attr(test, mutate)] fn total_collateral_value(supplier: &T::AccountId) -> Result, DispatchError> { let mut total_asset_value = Amount::::zero(T::ReferenceAssetId::get()); for (asset_id, _market) in Self::active_markets() { @@ -1372,6 +1408,7 @@ impl Pallet { Ok(total_asset_value) } + #[cfg_attr(test, mutate)] fn total_liquidation_threshold_value(borrower: &T::AccountId) -> Result, DispatchError> { let mut total_asset_value = Amount::::zero(T::ReferenceAssetId::get()); for (asset_id, _market) in Self::active_markets() { @@ -1384,6 +1421,7 @@ impl Pallet { /// Checks if the redeemer should be allowed to redeem tokens in given market. /// Takes into account both `free` and `locked` (i.e. deposited as collateral) lend_tokens of the redeemer. + #[cfg_attr(test, mutate)] fn redeem_allowed(redeemer: &T::AccountId, voucher: &Amount) -> DispatchResult { let asset_id = Self::underlying_id(voucher.currency())?; log::trace!( @@ -1414,6 +1452,7 @@ impl Pallet { } /// Borrower shouldn't borrow more than their total collateral value allows + #[cfg_attr(test, mutate)] fn borrow_allowed(borrower: &T::AccountId, borrow: &Amount) -> DispatchResult { Self::ensure_under_borrow_cap(borrow)?; Self::ensure_enough_cash(borrow)?; @@ -1424,6 +1463,7 @@ impl Pallet { } #[require_transactional] + #[cfg_attr(test, mutate)] fn do_repay_borrow_with_amount( borrower: &T::AccountId, asset_id: CurrencyId, @@ -1458,6 +1498,7 @@ impl Pallet { // Calculates and returns the most recent amount of borrowed balance of `currency_id` // for `who`. + #[cfg_attr(test, mutate)] pub fn current_borrow_balance(who: &T::AccountId, asset_id: CurrencyId) -> Result, DispatchError> { let snapshot: BorrowSnapshot> = Self::account_borrows(asset_id, who); if snapshot.principal.is_zero() || snapshot.borrow_index.is_zero() { @@ -1473,6 +1514,7 @@ impl Pallet { ) } + #[cfg_attr(test, mutate)] pub fn borrow_balance_from_old_and_new_index( old_index: &FixedU128, new_index: &FixedU128, @@ -1486,6 +1528,7 @@ impl Pallet { } /// Checks if the liquidation should be allowed to occur + #[cfg_attr(test, mutate)] fn liquidate_borrow_allowed( borrower: &T::AccountId, underlying: &Amount, @@ -1532,6 +1575,7 @@ impl Pallet { /// and liquidator will receive collateral_asset_id (as voucher amount) from /// borrower. #[require_transactional] + #[cfg_attr(test, mutate)] pub fn do_liquidate_borrow( liquidator: T::AccountId, borrower: T::AccountId, @@ -1575,6 +1619,7 @@ impl Pallet { } #[require_transactional] + #[cfg_attr(test, mutate)] fn liquidated_transfer( liquidator: &T::AccountId, borrower: &T::AccountId, @@ -1668,6 +1713,7 @@ impl Pallet { Ok(()) } + #[cfg_attr(test, mutate)] pub fn lock_if_account_deposited(account_id: &T::AccountId, lend_tokens: &Amount) -> DispatchResult { // if the receiver already has their collateral deposited let deposit = Pallet::::account_deposits(lend_tokens.currency(), account_id); @@ -1680,6 +1726,7 @@ impl Pallet { } // Ensures a given `asset_id` is an active market. + #[cfg_attr(test, mutate)] fn ensure_active_market(asset_id: CurrencyId) -> Result>, DispatchError> { Self::active_markets() .find(|(id, _)| id == &asset_id) @@ -1688,6 +1735,7 @@ impl Pallet { } /// Ensure supplying `amount` asset does not exceed the market's supply cap. + #[cfg_attr(test, mutate)] fn ensure_under_supply_cap(asset: &Amount) -> DispatchResult { let asset_id = asset.currency(); @@ -1704,6 +1752,7 @@ impl Pallet { } /// Ensure borrowing `amount` asset does not exceed the market's borrow cap. + #[cfg_attr(test, mutate)] fn ensure_under_borrow_cap(asset: &Amount) -> DispatchResult { let asset_id = asset.currency(); let market = Self::market(asset_id)?; @@ -1730,6 +1779,7 @@ impl Pallet { /// https://github.com/compound-finance/compound-protocol/blob/a3214f67b73310d547e00fc578e8355911c9d376/contracts/CToken.sol#L518 /// - but getCashPrior is the entire balance of the contract: /// https://github.com/compound-finance/compound-protocol/blob/a3214f67b73310d547e00fc578e8355911c9d376/contracts/CToken.sol#L1125 + #[cfg_attr(test, mutate)] fn ensure_enough_cash(amount: &Amount) -> DispatchResult { let reducible_cash = Self::get_total_cash(amount.currency()).checked_sub(&Self::total_reserves(amount.currency()))?; @@ -1741,6 +1791,7 @@ impl Pallet { } /// Ensures a given `lend_token_id` is unique in `Markets` and `UnderlyingAssetId`. + #[cfg_attr(test, mutate)] fn ensure_lend_token(lend_token_id: CurrencyId) -> DispatchResult { // The lend_token id is unique, cannot be repeated ensure!( @@ -1761,6 +1812,7 @@ impl Pallet { /// Returns `Err` If InsufficientLiquidity /// `account`: account that needs a liquidity check /// `reduce_amount`: amount to reduce the liquidity (collateral) of the `account` by + #[cfg_attr(test, mutate)] fn ensure_liquidity(account: &T::AccountId, reduce_amount: Amount) -> DispatchResult { if Self::get_account_liquidity(account)?.liquidity().ge(&reduce_amount)? { return Ok(()); @@ -1769,6 +1821,7 @@ impl Pallet { } /// Transferrable balance in the pallet account (`free - frozen`) + #[cfg_attr(test, mutate)] fn get_total_cash(asset_id: CurrencyId) -> Amount { Amount::new( orml_tokens::Pallet::::reducible_balance(asset_id, &Self::account_id(), true), @@ -1778,12 +1831,14 @@ impl Pallet { /// Get the total balance of `who`. /// Ignores any frozen balance of this account (`free + reserved`) + #[cfg_attr(test, mutate)] fn balance(asset_id: CurrencyId, who: &T::AccountId) -> Amount { let balance = as MultiCurrency>::total_balance(asset_id, who); Amount::new(balance, asset_id) } /// Total issuance of lending tokens (lend_tokens), given the underlying + #[cfg_attr(test, mutate)] pub fn total_supply(asset_id: CurrencyId) -> Result, DispatchError> { let lend_token_id = Self::lend_token_id(asset_id)?; let issuance = orml_tokens::Pallet::::total_issuance(lend_token_id); @@ -1791,6 +1846,7 @@ impl Pallet { } /// Free lending tokens (lend_tokens) of an account, given the underlying + #[cfg_attr(test, mutate)] pub fn free_lend_tokens(asset_id: CurrencyId, account_id: &T::AccountId) -> Result, DispatchError> { let lend_token_id = Self::lend_token_id(asset_id)?; let amount = Amount::new( @@ -1801,6 +1857,7 @@ impl Pallet { } /// Reserved lending tokens (lend_tokens) of an account, given the underlying + #[cfg_attr(test, mutate)] pub fn reserved_lend_tokens( asset_id: CurrencyId, account_id: &T::AccountId, @@ -1815,6 +1872,7 @@ impl Pallet { // Returns the value of the asset, in the reference currency. // Returns `Err` if oracle price not ready or arithmetic error. + #[cfg_attr(test, mutate)] pub fn get_asset_value(asset: &Amount) -> Result, DispatchError> { asset.convert_to(T::ReferenceAssetId::get()) } @@ -1822,6 +1880,7 @@ impl Pallet { // Returns a stored Market. // // Returns `Err` if market does not exist. + #[cfg_attr(test, mutate)] pub fn market(asset_id: CurrencyId) -> Result>, DispatchError> { Markets::::try_get(asset_id).map_err(|_err| Error::::MarketDoesNotExist.into()) } @@ -1829,6 +1888,7 @@ impl Pallet { // Mutates a stored Market. // // Returns `Err` if market does not exist. + #[cfg_attr(test, mutate)] pub(crate) fn mutate_market(asset_id: CurrencyId, cb: F) -> Result>, DispatchError> where F: FnOnce(&mut Market>) -> Market>, @@ -1842,6 +1902,7 @@ impl Pallet { } // All markets that are `MarketStatus::Active`. + #[cfg_attr(test, mutate)] fn active_markets() -> impl Iterator, Market>)> { Markets::::iter().filter(|(_, market)| market.state == MarketState::Active) } @@ -1849,6 +1910,7 @@ impl Pallet { // Returns the lend_token_id of the related asset // // Returns `Err` if market does not exist. + #[cfg_attr(test, mutate)] pub fn lend_token_id(asset_id: CurrencyId) -> Result, DispatchError> { if let Ok(market) = Self::market(asset_id) { Ok(market.lend_token_id) @@ -1858,12 +1920,14 @@ impl Pallet { } // Returns the incentive reward account + #[cfg_attr(test, mutate)] pub fn incentive_reward_account_id() -> T::AccountId { T::PalletId::get().into_sub_account_truncating(INCENTIVE_SUB_ACCOUNT) } } impl LoansTrait, AccountIdOf, Amount> for Pallet { + #[cfg_attr(test, mutate)] fn do_mint(supplier: &AccountIdOf, amount: &Amount) -> Result<(), DispatchError> { let asset_id = amount.currency(); Self::ensure_active_market(asset_id)?; @@ -1890,6 +1954,7 @@ impl LoansTrait, AccountIdOf, Amount> for Pallet< Ok(()) } + #[cfg_attr(test, mutate)] fn do_borrow(borrower: &AccountIdOf, borrow: &Amount) -> Result<(), DispatchError> { let asset_id = borrow.currency(); Self::ensure_active_market(asset_id)?; @@ -1924,6 +1989,7 @@ impl LoansTrait, AccountIdOf, Amount> for Pallet< Ok(()) } + #[cfg_attr(test, mutate)] fn do_deposit_collateral(supplier: &AccountIdOf, lend_token_amount: &Amount) -> Result<(), DispatchError> { // If the given asset_id is not a valid lend_token, fetching the underlying will fail let underlying_id = Self::underlying_id(lend_token_amount.currency())?; @@ -1945,6 +2011,7 @@ impl LoansTrait, AccountIdOf, Amount> for Pallet< Ok(()) } + #[cfg_attr(test, mutate)] fn do_withdraw_collateral(supplier: &AccountIdOf, voucher: &Amount) -> Result<(), DispatchError> { // If the given asset_id is not a valid lend_token, fetching the underlying will fail let underlying_id = Self::underlying_id(voucher.currency())?; @@ -1990,6 +2057,7 @@ impl LoansTrait, AccountIdOf, Amount> for Pallet< Ok(()) } + #[cfg_attr(test, mutate)] fn do_repay_borrow(borrower: &AccountIdOf, borrow: &Amount) -> Result<(), DispatchError> { let asset_id = borrow.currency(); Self::ensure_active_market(asset_id)?; @@ -2004,6 +2072,7 @@ impl LoansTrait, AccountIdOf, Amount> for Pallet< Ok(()) } + #[cfg_attr(test, mutate)] fn do_redeem(supplier: &AccountIdOf, underlying: &Amount, voucher: &Amount) -> Result<(), DispatchError> { let asset_id = underlying.currency(); @@ -2042,6 +2111,7 @@ impl LoansTrait, AccountIdOf, Amount> for Pallet< } // NOTE: used in OracleApi, so don't use oracle calls here or it'll recurse forever + #[cfg_attr(test, mutate)] fn recompute_underlying_amount(lend_tokens: &Amount) -> Result, DispatchError> { // This function could be called externally to this pallet, with interest // possibly not having accrued for a few blocks. This would result in using an @@ -2056,11 +2126,13 @@ impl LoansTrait, AccountIdOf, Amount> for Pallet< // Returns a stored asset_id // // Returns `Err` if asset_id does not exist, it also means that lend_token_id is invalid. + #[cfg_attr(test, mutate)] fn underlying_id(lend_token_id: CurrencyId) -> Result, DispatchError> { UnderlyingAssetId::::try_get(lend_token_id).map_err(|_err| Error::::InvalidLendTokenId.into()) } // NOTE: used in OracleApi, so don't use oracle calls here or it'll recurse forever + #[cfg_attr(test, mutate)] fn recompute_collateral_amount(underlying: &Amount) -> Result, DispatchError> { // This function could be called externally to this pallet, with interest // possibly not having accrued for a few blocks. This would result in using an @@ -2076,6 +2148,7 @@ impl LoansTrait, AccountIdOf, Amount> for Pallet< } impl LoansMarketDataProvider, BalanceOf> for Pallet { + #[cfg_attr(test, mutate)] fn get_market_info(asset_id: CurrencyId) -> Result { let market = Self::market(asset_id)?; let full_rate = Self::get_full_interest_rate(asset_id).ok_or(Error::::InvalidRateModelParam)?; @@ -2088,6 +2161,7 @@ impl LoansMarketDataProvider, BalanceOf> for Pallet< }) } + #[cfg_attr(test, mutate)] fn get_market_status(asset_id: CurrencyId) -> Result>, DispatchError> { let (borrow_rate, supply_rate, exchange_rate, utilization, total_borrows, total_reserves, borrow_index) = Self::get_market_status(asset_id)?; @@ -2102,6 +2176,7 @@ impl LoansMarketDataProvider, BalanceOf> for Pallet< }) } + #[cfg_attr(test, mutate)] fn get_full_interest_rate(asset_id: CurrencyId) -> Option { if let Ok(market) = Self::market(asset_id) { let rate = match market.rate_model { @@ -2115,6 +2190,7 @@ impl LoansMarketDataProvider, BalanceOf> for Pallet< } impl OnExchangeRateChange> for Pallet { + #[cfg_attr(test, mutate)] fn on_exchange_rate_change(currency_id: &CurrencyId) { // todo: propagate error if let Ok(lend_token_id) = Pallet::::lend_token_id(*currency_id) { diff --git a/crates/loans/src/rate_model.rs b/crates/loans/src/rate_model.rs index cc17bb45f5..54c4075237 100644 --- a/crates/loans/src/rate_model.rs +++ b/crates/loans/src/rate_model.rs @@ -31,6 +31,7 @@ pub enum InterestRateModel { } impl Default for InterestRateModel { + #[cfg_attr(test, mutate)] fn default() -> Self { Self::new_jump_model( Rate::saturating_from_rational(2, 100), @@ -42,14 +43,17 @@ impl Default for InterestRateModel { } impl InterestRateModel { + #[cfg_attr(test, mutate)] pub fn new_jump_model(base_rate: Rate, jump_rate: Rate, full_rate: Rate, jump_utilization: Ratio) -> Self { Self::Jump(JumpModel::new_model(base_rate, jump_rate, full_rate, jump_utilization)) } + #[cfg_attr(test, mutate)] pub fn new_curve_model(base_rate: Rate) -> Self { Self::Curve(CurveModel::new_model(base_rate)) } + #[cfg_attr(test, mutate)] pub fn check_model(&self) -> bool { match self { Self::Jump(jump) => jump.check_model(), @@ -58,6 +62,7 @@ impl InterestRateModel { } /// Calculates the current borrow interest rate + #[cfg_attr(test, mutate)] pub fn get_borrow_rate(&self, utilization: Ratio) -> Option { match self { Self::Jump(jump) => jump.get_borrow_rate(utilization), @@ -66,6 +71,7 @@ impl InterestRateModel { } /// Calculates the current supply interest rate + #[cfg_attr(test, mutate)] pub fn get_supply_rate(borrow_rate: Rate, util: Ratio, reserve_factor: Ratio) -> Rate { // ((1 - reserve_factor) * borrow_rate) * utilization let one_minus_reserve_factor = Ratio::one().saturating_sub(reserve_factor); @@ -95,6 +101,7 @@ impl JumpModel { pub const MAX_FULL_RATE: Rate = Rate::from_inner(500_000_000_000_000_000); // 50% /// Create a new rate model + #[cfg_attr(test, mutate)] pub fn new_model(base_rate: Rate, jump_rate: Rate, full_rate: Rate, jump_utilization: Ratio) -> JumpModel { Self { base_rate, @@ -105,6 +112,7 @@ impl JumpModel { } /// Check the jump model for sanity + #[cfg_attr(test, mutate)] pub fn check_model(&self) -> bool { if self.base_rate > Self::MAX_BASE_RATE || self.jump_rate > Self::MAX_JUMP_RATE @@ -120,6 +128,7 @@ impl JumpModel { } /// Calculates the borrow interest rate of jump model + #[cfg_attr(test, mutate)] pub fn get_borrow_rate(&self, utilization: Ratio) -> Option { if utilization <= self.jump_utilization { // utilization * (jump_rate - zero_rate) / jump_utilization + zero_rate @@ -157,16 +166,19 @@ impl CurveModel { pub const MAX_BASE_RATE: Rate = Rate::from_inner(100_000_000_000_000_000); // 10% /// Create a new curve model + #[cfg_attr(test, mutate)] pub fn new_model(base_rate: Rate) -> CurveModel { Self { base_rate } } /// Check the curve model for sanity + #[cfg_attr(test, mutate)] pub fn check_model(&self) -> bool { self.base_rate <= Self::MAX_BASE_RATE } /// Calculates the borrow interest rate of curve model + #[cfg_attr(test, mutate)] pub fn get_borrow_rate(&self, utilization: Ratio) -> Option { const NINE: usize = 9; let utilization_rate: Rate = utilization.into(); diff --git a/crates/loans/src/types.rs b/crates/loans/src/types.rs index 871ab6765d..a3fc892afb 100644 --- a/crates/loans/src/types.rs +++ b/crates/loans/src/types.rs @@ -5,6 +5,9 @@ use frame_support::pallet_prelude::*; use primitives::{CurrencyId, Liquidity, Rate, Ratio, Shortfall}; use scale_info::TypeInfo; +#[cfg(test)] +use mutagen::mutate; + // TODO: `cargo doc` crashes on this type, remove the `hidden` macro // when upgrading rustc in case that fixes it /// Container for account liquidity information @@ -16,6 +19,7 @@ pub enum AccountLiquidity { } impl AccountLiquidity { + #[cfg_attr(test, mutate)] pub fn from_collateral_and_debt( collateral_value: Amount, borrow_value: Amount, @@ -28,12 +32,14 @@ impl AccountLiquidity { Ok(account_liquidity) } + #[cfg_attr(test, mutate)] pub fn currency(&self) -> CurrencyId { match &self { AccountLiquidity::Liquidity(x) | AccountLiquidity::Shortfall(x) => x.currency(), } } + #[cfg_attr(test, mutate)] pub fn liquidity(&self) -> Amount { if let AccountLiquidity::Liquidity(x) = &self { return x.clone(); @@ -41,6 +47,7 @@ impl AccountLiquidity { Amount::::zero(self.currency()) } + #[cfg_attr(test, mutate)] pub fn shortfall(&self) -> Amount { if let AccountLiquidity::Shortfall(x) = &self { return x.clone(); @@ -48,6 +55,7 @@ impl AccountLiquidity { Amount::::zero(self.currency()) } + #[cfg_attr(test, mutate)] pub fn to_rpc_tuple(&self) -> Result<(Liquidity, Shortfall), DispatchError> { Ok(( self.liquidity().to_unsigned_fixed_point()?,