diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 4b572a56c6..52ee539c27 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -60,7 +60,9 @@ jobs: - name: upload Docs files uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 #v4.3.0 with: + name: github-pages path: ./target/doc + retention-days: 1 - name: Deploy Docs # if: github.ref == 'refs/heads/main' diff --git a/pallets/loans/src/benchmarking.rs b/pallets/loans/src/benchmarking.rs index e691df9543..7ce84d45a5 100644 --- a/pallets/loans/src/benchmarking.rs +++ b/pallets/loans/src/benchmarking.rs @@ -60,7 +60,7 @@ type MaxRateCountOf = <::InterestAccrual as InterestAccrual< fn config_mocks() { use cfg_mocks::pallet_mock_data::util::MockDataCollection; - use crate::tests::mock::{MockChangeGuard, MockPermissions, MockPools, MockPrices}; + use crate::tests::mock::{MockChangeGuard, MockPermissions, MockPools, MockPrices, MockTimer}; MockPermissions::mock_add(|_, _, _| Ok(())); MockPermissions::mock_has(|_, _, _| true); @@ -74,6 +74,7 @@ fn config_mocks() { MockChangeGuard::mock_released(move |_, _| Ok(change.clone())); Ok(sp_core::H256::default()) }); + MockTimer::mock_now(|| 0); } struct Helper(sp_std::marker::PhantomData); diff --git a/pallets/loans/src/entities/input.rs b/pallets/loans/src/entities/input.rs index a858250de4..4e5fe975e0 100644 --- a/pallets/loans/src/entities/input.rs +++ b/pallets/loans/src/entities/input.rs @@ -7,6 +7,7 @@ use crate::{ entities::pricing::external::ExternalAmount, pallet::{Config, Error}, types::RepaidAmount, + PriceOf, }; #[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)] @@ -61,6 +62,6 @@ impl RepaidInput { #[scale_info(skip_type_params(T))] pub enum PriceCollectionInput { Empty, - Custom(BoundedBTreeMap), + Custom(BoundedBTreeMap, T::MaxActiveLoansPerPool>), FromRegistry, } diff --git a/pallets/loans/src/entities/loans.rs b/pallets/loans/src/entities/loans.rs index 4731100fdb..2ae2e6c90a 100644 --- a/pallets/loans/src/entities/loans.rs +++ b/pallets/loans/src/entities/loans.rs @@ -32,6 +32,7 @@ use crate::{ BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanRestrictions, MutationError, RepaidAmount, RepayLoanError, RepayRestrictions, }, + PriceOf, }; /// Loan information. @@ -227,7 +228,7 @@ impl ActiveLoan { self.origination_date } - pub fn maturity_date(&self) -> Seconds { + pub fn maturity_date(&self) -> Option { self.schedule.maturity.date() } @@ -275,9 +276,10 @@ impl ActiveLoan { ) -> Result { let now = T::Time::now(); match trigger { - WriteOffTrigger::PrincipalOverdue(overdue_secs) => { - Ok(now >= self.maturity_date().ensure_add(*overdue_secs)?) - } + WriteOffTrigger::PrincipalOverdue(overdue_secs) => match self.maturity_date() { + Some(maturity) => Ok(now >= maturity.ensure_add(*overdue_secs)?), + None => Ok(false), + }, WriteOffTrigger::PriceOutdated(secs) => match &self.pricing { ActivePricing::External(pricing) => { Ok(now >= pricing.last_updated(pool_id).ensure_add(*secs)?) @@ -306,7 +308,7 @@ impl ActiveLoan { pub fn present_value_by( &self, rates: &Rates, - prices: &BTreeMap, + prices: &BTreeMap>, ) -> Result where Rates: RateCollection, @@ -599,3 +601,101 @@ impl TryFrom<(T::PoolId, ActiveLoan)> for ActiveLoanInfo { }) } } + +/// Adds `with_linear_pricing` to ExternalPricing struct for migration to v4 +pub mod v3 { + use cfg_traits::{interest::InterestRate, Seconds}; + use parity_scale_codec::{Decode, Encode}; + + use crate::{ + entities::{ + loans::BlockNumberFor, + pricing::external::v3::{ActivePricing, Pricing}, + }, + types::{cashflow::RepaymentSchedule, LoanRestrictions, RepaidAmount}, + AssetOf, Config, + }; + + #[derive(Encode, Decode)] + pub struct ActiveLoan { + schedule: RepaymentSchedule, + collateral: AssetOf, + restrictions: LoanRestrictions, + borrower: T::AccountId, + write_off_percentage: T::Rate, + origination_date: Seconds, + pricing: ActivePricing, + total_borrowed: T::Balance, + total_repaid: RepaidAmount, + repayments_on_schedule_until: Seconds, + } + + impl ActiveLoan { + pub fn migrate(self, with_linear_pricing: bool) -> super::ActiveLoan { + super::ActiveLoan { + schedule: self.schedule, + collateral: self.collateral, + restrictions: self.restrictions, + borrower: self.borrower, + write_off_percentage: self.write_off_percentage, + origination_date: self.origination_date, + pricing: self.pricing.migrate(with_linear_pricing), + total_borrowed: self.total_borrowed, + total_repaid: self.total_repaid, + repayments_on_schedule_until: self.repayments_on_schedule_until, + } + } + } + + #[derive(Encode, Decode)] + pub struct CreatedLoan { + info: LoanInfo, + borrower: T::AccountId, + } + + impl CreatedLoan { + pub fn migrate(self, with_linear_pricing: bool) -> super::CreatedLoan { + super::CreatedLoan::::new(self.info.migrate(with_linear_pricing), self.borrower) + } + } + + #[derive(Encode, Decode)] + pub struct ClosedLoan { + closed_at: BlockNumberFor, + info: LoanInfo, + total_borrowed: T::Balance, + total_repaid: RepaidAmount, + } + + impl ClosedLoan { + pub fn migrate(self, with_linear_pricing: bool) -> super::ClosedLoan { + super::ClosedLoan:: { + closed_at: self.closed_at, + info: self.info.migrate(with_linear_pricing), + total_borrowed: self.total_borrowed, + total_repaid: self.total_repaid, + } + } + } + + #[derive(Encode, Decode)] + pub struct LoanInfo { + pub schedule: RepaymentSchedule, + pub collateral: AssetOf, + pub interest_rate: InterestRate, + pub pricing: Pricing, + pub restrictions: LoanRestrictions, + } + + impl LoanInfo { + pub fn migrate(self, with_linear_pricing: bool) -> super::LoanInfo { + super::LoanInfo:: { + pricing: self.pricing.migrate(with_linear_pricing), + schedule: self.schedule, + collateral: self.collateral, + interest_rate: self.interest_rate, + restrictions: self.restrictions, + } + } + } +} diff --git a/pallets/loans/src/entities/pricing/external.rs b/pallets/loans/src/entities/pricing/external.rs index d0ecc8e897..e9652ac518 100644 --- a/pallets/loans/src/entities/pricing/external.rs +++ b/pallets/loans/src/entities/pricing/external.rs @@ -9,11 +9,12 @@ use sp_runtime::{ traits::{EnsureAdd, EnsureFixedPointNumber, EnsureSub, Zero}, ArithmeticError, DispatchError, DispatchResult, FixedPointNumber, }; -use sp_std::collections::btree_map::BTreeMap; +use sp_std::{cmp::min, collections::btree_map::BTreeMap}; use crate::{ entities::interest::ActiveInterestRate, pallet::{Config, Error}, + PriceOf, }; #[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)] @@ -76,6 +77,9 @@ pub struct ExternalPricing { /// borrow/repay and the current oracle price. /// See [`ExternalAmount::settlement_price`]. pub max_price_variation: T::Rate, + + /// If the pricing is estimated with a linear pricing model. + pub with_linear_pricing: bool, } impl ExternalPricing { @@ -150,29 +154,64 @@ impl ExternalActivePricing { } } - fn linear_accrual_price(&self, maturity: Seconds) -> Result { - Ok(cfg_utils::math::y_coord_in_rect( - (self.settlement_price_updated, self.latest_settlement_price), - (maturity, self.info.notional), - T::Time::now(), - )?) + fn maybe_with_linear_accrual_price( + &self, + maturity: Option, + price: T::Balance, + price_last_updated: Seconds, + ) -> Result { + if let (Some(maturity), true) = (maturity, self.info.with_linear_pricing) { + if min(price_last_updated, maturity) == maturity { + // We can not have 2 'xs' with different 'y' in a rect. + // That only happens at maturity + return Ok(self.info.notional); + } + + return Ok(cfg_utils::math::y_coord_in_rect( + (min(price_last_updated, maturity), price), + (maturity, self.info.notional), + min(T::Time::now(), maturity), + )?); + } + + Ok(price) } pub fn current_price( &self, pool_id: T::PoolId, - maturity: Seconds, + maturity: Option, ) -> Result { - Ok(match T::PriceRegistry::get(&self.info.price_id, &pool_id) { - Ok(data) => data.0, - Err(_) => self.linear_accrual_price(maturity)?, - }) + self.current_price_inner( + maturity, + T::PriceRegistry::get(&self.info.price_id, &pool_id).ok(), + ) + } + + fn current_price_inner( + &self, + maturity: Option, + oracle: Option>, + ) -> Result { + if let Some((oracle_price, oracle_provided_at)) = oracle { + self.maybe_with_linear_accrual_price( + maturity, + oracle_price, + oracle_provided_at.into_seconds(), + ) + } else { + self.maybe_with_linear_accrual_price( + maturity, + self.latest_settlement_price, + self.settlement_price_updated, + ) + } } pub fn outstanding_principal( &self, pool_id: T::PoolId, - maturity: Seconds, + maturity: Option, ) -> Result { let price = self.current_price(pool_id, maturity)?; Ok(self.outstanding_quantity.ensure_mul_int(price)?) @@ -190,20 +229,17 @@ impl ExternalActivePricing { pub fn present_value( &self, pool_id: T::PoolId, - maturity: Seconds, + maturity: Option, ) -> Result { self.outstanding_principal(pool_id, maturity) } pub fn present_value_cached( &self, - cache: &BTreeMap, - maturity: Seconds, + cache: &BTreeMap>, + maturity: Option, ) -> Result { - let price = match cache.get(&self.info.price_id) { - Some(data) => *data, - None => self.linear_accrual_price(maturity)?, - }; + let price = self.current_price_inner(maturity, cache.get(&self.info.price_id).copied())?; Ok(self.outstanding_quantity.ensure_mul_int(price)?) } @@ -288,3 +324,95 @@ impl ExternalActivePricing { Ok(()) } } + +/// Adds `with_linear_pricing` to ExternalPricing struct for migration to v4 +pub mod v3 { + use cfg_traits::Seconds; + use parity_scale_codec::{Decode, Encode}; + + use crate::{ + entities::{ + interest::ActiveInterestRate, + pricing::{external::MaxBorrowAmount, internal, internal::InternalActivePricing}, + }, + Config, + }; + + #[derive(Encode, Decode)] + pub enum Pricing { + Internal(internal::InternalPricing), + External(ExternalPricing), + } + + impl Pricing { + pub fn migrate(self, with_linear_pricing: bool) -> crate::entities::pricing::Pricing { + match self { + Pricing::Internal(i) => crate::entities::pricing::Pricing::Internal(i), + Pricing::External(e) => { + crate::entities::pricing::Pricing::External(e.migrate(with_linear_pricing)) + } + } + } + } + + #[derive(Encode, Decode)] + pub struct ExternalPricing { + pub price_id: T::PriceId, + pub max_borrow_amount: MaxBorrowAmount, + pub notional: T::Balance, + pub max_price_variation: T::Rate, + } + + #[derive(Encode, Decode)] + pub enum ActivePricing { + Internal(InternalActivePricing), + External(ExternalActivePricing), + } + + impl ActivePricing { + pub fn migrate( + self, + with_linear_pricing: bool, + ) -> crate::entities::pricing::ActivePricing { + match self { + ActivePricing::Internal(i) => crate::entities::pricing::ActivePricing::Internal(i), + ActivePricing::External(e) => crate::entities::pricing::ActivePricing::External( + e.migrate(with_linear_pricing), + ), + } + } + } + + #[derive(Encode, Decode)] + pub struct ExternalActivePricing { + info: ExternalPricing, + outstanding_quantity: T::Quantity, + pub interest: ActiveInterestRate, + latest_settlement_price: T::Balance, + settlement_price_updated: Seconds, + } + + impl ExternalActivePricing { + pub fn migrate(self, with_linear_pricing: bool) -> super::ExternalActivePricing { + super::ExternalActivePricing { + info: self.info.migrate(with_linear_pricing), + outstanding_quantity: self.outstanding_quantity, + interest: self.interest, + latest_settlement_price: self.latest_settlement_price, + settlement_price_updated: self.settlement_price_updated, + } + } + } + + impl ExternalPricing { + pub fn migrate(self, with_linear_pricing: bool) -> super::ExternalPricing { + super::ExternalPricing { + price_id: self.price_id, + max_borrow_amount: self.max_borrow_amount, + notional: self.notional, + max_price_variation: self.max_price_variation, + with_linear_pricing, + } + } + } +} diff --git a/pallets/loans/src/entities/pricing/internal.rs b/pallets/loans/src/entities/pricing/internal.rs index 1c285ce261..10ea0d779a 100644 --- a/pallets/loans/src/entities/pricing/internal.rs +++ b/pallets/loans/src/entities/pricing/internal.rs @@ -90,10 +90,13 @@ impl InternalActivePricing { &self, debt: T::Balance, origination_date: Seconds, - maturity_date: Seconds, + maturity_date: Option, ) -> Result { match &self.info.valuation_method { ValuationMethod::DiscountedCashFlow(dcf) => { + let maturity_date = + maturity_date.ok_or(Error::::MaturityDateNeededForValuationMethod)?; + let now = T::Time::now(); Ok(dcf.compute_present_value( debt, @@ -110,7 +113,7 @@ impl InternalActivePricing { pub fn present_value( &self, origination_date: Seconds, - maturity_date: Seconds, + maturity_date: Option, ) -> Result { let debt = self.interest.current_debt()?; self.compute_present_value(debt, origination_date, maturity_date) @@ -120,7 +123,7 @@ impl InternalActivePricing { &self, cache: &Rates, origination_date: Seconds, - maturity_date: Seconds, + maturity_date: Option, ) -> Result where Rates: RateCollection, diff --git a/pallets/loans/src/lib.rs b/pallets/loans/src/lib.rs index 9369ab7e3a..7d31611682 100644 --- a/pallets/loans/src/lib.rs +++ b/pallets/loans/src/lib.rs @@ -119,7 +119,7 @@ pub mod pallet { pub type AssetOf = (::CollectionId, ::ItemId); pub type PriceOf = (::Balance, ::Moment); - const STORAGE_VERSION: StorageVersion = StorageVersion::new(3); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] @@ -170,7 +170,7 @@ pub mod pallet { type Time: TimeAsSecs; /// Generic time type - type Moment: Parameter + Member + IntoSeconds; + type Moment: Parameter + Member + Copy + IntoSeconds; /// Used to mint, transfer, and inspect assets. type NonFungible: Transfer @@ -233,7 +233,7 @@ pub mod pallet { /// Storage for loans that has been created but are not still active. #[pallet::storage] - pub(crate) type CreatedLoan = StorageDoubleMap< + pub type CreatedLoan = StorageDoubleMap< _, Blake2_128Concat, T::PoolId, @@ -260,7 +260,7 @@ pub mod pallet { /// No mutations are expected in this storage. /// Loans are stored here for historical purposes. #[pallet::storage] - pub(crate) type ClosedLoan = StorageDoubleMap< + pub type ClosedLoan = StorageDoubleMap< _, Blake2_128Concat, T::PoolId, @@ -399,6 +399,10 @@ pub mod pallet { TransferDebtToSameLoan, /// Emits when debt is transfered with different repaid/borrow amounts TransferDebtAmountMismatched, + /// Emits when the loan has no maturity date set, but the valuation + /// method needs one. Making valuation and maturity settings + /// incompatible. + MaturityDateNeededForValuationMethod, } impl From for Error { @@ -1051,7 +1055,7 @@ pub mod pallet { pub fn registered_prices( pool_id: T::PoolId, - ) -> Result, DispatchError> { + ) -> Result>, DispatchError> { let collection = T::PriceRegistry::collection(&pool_id)?; Ok(ActiveLoans::::get(pool_id) .iter() @@ -1059,7 +1063,7 @@ pub mod pallet { .filter_map(|price_id| { collection .get(&price_id) - .map(|price| (price_id, price.0)) + .map(|price| (price_id, (price.0, price.1))) .ok() }) .collect::>()) diff --git a/pallets/loans/src/tests/borrow_loan.rs b/pallets/loans/src/tests/borrow_loan.rs index c8eea0974e..67b0648271 100644 --- a/pallets/loans/src/tests/borrow_loan.rs +++ b/pallets/loans/src/tests/borrow_loan.rs @@ -648,7 +648,10 @@ mod cashflow { loan.origination_date(), secs_from_ymdhms(1970, 1, 1, 0, 0, 10) ); - assert_eq!(loan.maturity_date(), secs_from_ymdhms(1971, 1, 1, 0, 0, 10)); + assert_eq!( + loan.maturity_date(), + Some(secs_from_ymdhms(1971, 1, 1, 0, 0, 10)) + ); let principal = (COLLATERAL_VALUE / 2) / 12; let interest_rate_per_month = DEFAULT_INTEREST_RATE / 12.0; diff --git a/pallets/loans/src/tests/mock.rs b/pallets/loans/src/tests/mock.rs index c2cc4e8ce3..ae472f73a5 100644 --- a/pallets/loans/src/tests/mock.rs +++ b/pallets/loans/src/tests/mock.rs @@ -22,7 +22,7 @@ use frame_support::{ derive_impl, traits::{ tokens::nonfungibles::{Create, Mutate}, - AsEnsureOriginWithArg, ConstU64, Hooks, UnixTime, + AsEnsureOriginWithArg, Hooks, UnixTime, }, }; use frame_system::{EnsureRoot, EnsureSigned}; @@ -64,11 +64,14 @@ pub const POOL_OTHER_ACCOUNT: AccountId = 100; pub const COLLATERAL_VALUE: Balance = 10000; pub const DEFAULT_INTEREST_RATE: f64 = 0.5; +pub const DEFAULT_DISCOUNT_RATE: f64 = 0.02; +pub const DEFAULT_PROBABILITY_OF_DEFAULT: f64 = 0.1; +pub const DEFAULT_LOSS_GIVEN_DEFAULT: f64 = 0.5; pub const POLICY_PERCENTAGE: f64 = 0.5; pub const POLICY_PENALTY: f64 = 0.5; pub const REGISTER_PRICE_ID: PriceId = 42; pub const UNREGISTER_PRICE_ID: PriceId = 88; -pub const PRICE_VALUE: Balance = 998; +pub const PRICE_VALUE: Balance = 980; pub const NOTIONAL: Balance = 1000; pub const QUANTITY: Quantity = Quantity::from_rational(12, 1); pub const CHANGE_ID: ChangeId = H256::repeat_byte(0x42); @@ -93,7 +96,7 @@ pub type ChangeId = H256; frame_support::construct_runtime!( pub enum Runtime { System: frame_system, - Timer: pallet_timestamp, + MockTimer: cfg_mocks::time::pallet, Balances: pallet_balances, Uniques: pallet_uniques, InterestAccrual: pallet_interest_accrual, @@ -117,11 +120,8 @@ impl frame_system::Config for Runtime { type Block = frame_system::mocking::MockBlock; } -impl pallet_timestamp::Config for Runtime { - type MinimumPeriod = ConstU64; +impl cfg_mocks::time::pallet::Config for Runtime { type Moment = Millis; - type OnTimestampSet = (); - type WeightInfo = (); } #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)] @@ -159,7 +159,7 @@ impl pallet_interest_accrual::Config for Runtime { type MaxRateCount = MaxActiveLoansPerPool; type Rate = Rate; type RuntimeEvent = RuntimeEvent; - type Time = Timer; + type Time = MockTimer; type Weights = (); } @@ -212,14 +212,14 @@ impl pallet_loans::Config for Runtime { type Rate = Rate; type RuntimeChange = Change; type RuntimeEvent = RuntimeEvent; - type Time = Timer; + type Time = MockTimer; type WeightInfo = (); } pub fn new_test_ext() -> sp_io::TestExternalities { let mut ext = System::externalities(); ext.execute_with(|| { - advance_time(BLOCK_TIME); + MockTimer::mock_now(|| BLOCK_TIME.as_millis() as u64); Uniques::create_collection(&COLLECTION_A, &BORROWER, &ASSET_COLLECTION_OWNER).unwrap(); Uniques::mint_into(&COLLECTION_A, &ASSET_AA.1, &BORROWER).unwrap(); @@ -234,10 +234,11 @@ pub fn new_test_ext() -> sp_io::TestExternalities { } pub fn now() -> Duration { - ::now() + ::now() } pub fn advance_time(elapsed: Duration) { - Timer::set_timestamp(Timer::get() + elapsed.as_millis() as u64); + let before = now(); + MockTimer::mock_now(move || (before + elapsed).as_millis() as u64); InterestAccrual::on_initialize(0); } diff --git a/pallets/loans/src/tests/mod.rs b/pallets/loans/src/tests/mod.rs index 628d212eef..b2f6c3f4b6 100644 --- a/pallets/loans/src/tests/mod.rs +++ b/pallets/loans/src/tests/mod.rs @@ -5,13 +5,16 @@ use cfg_primitives::SECONDS_PER_DAY; use cfg_traits::interest::{CompoundingSchedule, InterestRate}; use cfg_types::permissions::{PermissionScope, PoolRole, Role}; use frame_support::{assert_noop, assert_ok, storage::bounded_vec::BoundedVec}; -use sp_runtime::{traits::BadOrigin, DispatchError, FixedPointNumber}; +use sp_runtime::{ + traits::{BadOrigin, One}, + DispatchError, FixedPointNumber, +}; use super::{ entities::{ changes::{Change, InternalMutation, LoanMutation}, input::{PrincipalInput, RepaidInput}, - loans::{ActiveLoan, LoanInfo}, + loans::{ActiveLoan, ActiveLoanInfo, LoanInfo}, pricing::{ external::{ExternalAmount, ExternalPricing, MaxBorrowAmount as ExtMaxBorrowAmount}, internal::{InternalPricing, MaxBorrowAmount as IntMaxBorrowAmount}, diff --git a/pallets/loans/src/tests/portfolio_valuation.rs b/pallets/loans/src/tests/portfolio_valuation.rs index af1b548723..a6887adde4 100644 --- a/pallets/loans/src/tests/portfolio_valuation.rs +++ b/pallets/loans/src/tests/portfolio_valuation.rs @@ -198,14 +198,16 @@ fn with_unregister_price_id_and_oracle_not_required() { expected_portfolio(QUANTITY.saturating_mul_int(price_value_after_half_year)); // Suddenty, the oracle set a value + const MARKET_PRICE_VALUE: Balance = 999; MockPrices::mock_collection(|_| { Ok(MockDataCollection::new(|_| { - Ok((PRICE_VALUE * 8, BLOCK_TIME_MS)) + Ok((MARKET_PRICE_VALUE, BLOCK_TIME_MS)) })) }); + let price_value_after_half_year = MARKET_PRICE_VALUE + (NOTIONAL - MARKET_PRICE_VALUE) / 2; update_portfolio(); - expected_portfolio(QUANTITY.saturating_mul_int(PRICE_VALUE * 8)); + expected_portfolio(QUANTITY.saturating_mul_int(price_value_after_half_year)); }); } @@ -218,3 +220,112 @@ fn empty_portfolio_with_current_timestamp() { ); }); } + +#[test] +fn no_linear_pricing_either_settlement_or_oracle() { + new_test_ext().execute_with(|| { + let mut external_pricing = util::base_external_pricing(); + external_pricing.with_linear_pricing = false; + external_pricing.max_price_variation = Rate::one(); + let loan = LoanInfo { + pricing: Pricing::External(ExternalPricing { + price_id: UNREGISTER_PRICE_ID, + ..external_pricing + }), + ..util::base_external_loan() + }; + let loan_1 = util::create_loan(loan); + const SETTLEMENT_PRICE: Balance = 970; + let amount = ExternalAmount::new(QUANTITY, SETTLEMENT_PRICE); + config_mocks(); + + util::borrow_loan(loan_1, PrincipalInput::External(amount.clone())); + + advance_time(YEAR / 2); + + const MARKET_PRICE_VALUE: Balance = 999; + MockPrices::mock_collection(|_| { + Ok(MockDataCollection::new(|_| { + Ok((MARKET_PRICE_VALUE, BLOCK_TIME_MS)) + })) + }); + + update_portfolio(); + expected_portfolio(QUANTITY.saturating_mul_int(MARKET_PRICE_VALUE)); + + MockPrices::mock_collection(|pool_id| { + assert_eq!(*pool_id, POOL_A); + Ok(MockDataCollection::new(|_| Err(PRICE_ID_NO_FOUND))) + }); + + update_portfolio(); + expected_portfolio(QUANTITY.saturating_mul_int(SETTLEMENT_PRICE)); + + MockPrices::mock_collection(|_| { + Ok(MockDataCollection::new(|_| { + Ok((MARKET_PRICE_VALUE, BLOCK_TIME_MS)) + })) + }); + update_portfolio(); + expected_portfolio(QUANTITY.saturating_mul_int(MARKET_PRICE_VALUE)); + }); +} + +#[test] +fn internal_dcf_with_no_maturity() { + new_test_ext().execute_with(|| { + let mut internal = util::dcf_internal_loan(); + internal.schedule.maturity = Maturity::None; + + let loan_id = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..internal + }); + + MockPools::mock_withdraw(|_, _, _| Ok(())); + + assert_noop!( + Loans::borrow( + RuntimeOrigin::signed(util::borrower(loan_id)), + POOL_A, + loan_id, + PrincipalInput::Internal(COLLATERAL_VALUE), + ), + Error::::MaturityDateNeededForValuationMethod + ); + }); +} + +#[test] +fn internal_oustanding_debt_with_no_maturity() { + new_test_ext().execute_with(|| { + let mut internal = util::base_internal_loan(); + internal.schedule.maturity = Maturity::None; + + let loan_id = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..internal + }); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); + + config_mocks(); + let pv = util::current_loan_pv(loan_id); + update_portfolio(); + expected_portfolio(pv); + + advance_time(YEAR); + + update_portfolio(); + expected_portfolio( + Rate::from_float(util::interest_for(DEFAULT_INTEREST_RATE, YEAR)) + .checked_mul_int(COLLATERAL_VALUE) + .unwrap(), + ); + + util::repay_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); + + config_mocks(); + update_portfolio(); + expected_portfolio(0); + }); +} diff --git a/pallets/loans/src/tests/repay_loan.rs b/pallets/loans/src/tests/repay_loan.rs index 3e5fa8be08..87d88ba7c2 100644 --- a/pallets/loans/src/tests/repay_loan.rs +++ b/pallets/loans/src/tests/repay_loan.rs @@ -1,3 +1,5 @@ +use sp_arithmetic::traits::Saturating; + use super::*; pub fn config_mocks(deposit_amount: Balance) { @@ -270,6 +272,82 @@ fn with_more_than_required() { }); } +#[test] +fn with_more_than_required_external() { + new_test_ext().execute_with(|| { + let variation = Rate::one(); + let mut pricing = util::base_external_pricing(); + pricing.max_price_variation = variation; + let mut info = util::base_external_loan(); + info.pricing = Pricing::External(pricing); + + let loan_id = util::create_loan(info); + let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); + + let amount = ExternalAmount::new( + QUANTITY.saturating_mul(Quantity::from_rational(2, 1)), + PRICE_VALUE + variation.checked_mul_int(PRICE_VALUE).unwrap(), + ); + config_mocks_with_price(amount.balance().unwrap(), PRICE_VALUE); + + assert_noop!( + Loans::repay( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + RepaidInput { + principal: PrincipalInput::External(amount), + interest: 0, + unscheduled: 0, + }, + ), + Error::::from(RepayLoanError::MaxPrincipalAmountExceeded) + ); + + let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE * 2); + config_mocks_with_price(amount.balance().unwrap(), PRICE_VALUE); + + assert_ok!(Loans::repay( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + RepaidInput { + principal: PrincipalInput::External(amount.clone()), + interest: 0, + unscheduled: 0, + }, + )); + + config_mocks_with_price(0, PRICE_VALUE); + assert_noop!( + Loans::repay( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + RepaidInput { + principal: PrincipalInput::External(amount), + interest: 0, + unscheduled: 0, + } + ), + Error::::from(RepayLoanError::MaxPrincipalAmountExceeded) + ); + + MockPrices::mock_unregister_id(move |id, pool_id| { + assert_eq!(*pool_id, POOL_A); + assert_eq!(*id, REGISTER_PRICE_ID); + Ok(()) + }); + + assert_ok!(Loans::close( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + )); + }); +} + #[test] fn with_restriction_full_once() { new_test_ext().execute_with(|| { @@ -823,3 +901,80 @@ fn with_unregister_price_id_and_oracle_not_required() { ); }); } + +#[test] +fn with_external_pricing() { + new_test_ext().execute_with(|| { + let loan_id = util::create_loan(LoanInfo { + pricing: Pricing::External(ExternalPricing { + price_id: UNREGISTER_PRICE_ID, + ..util::base_external_pricing() + }), + ..util::base_external_loan() + }); + + let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); + + let amount = ExternalAmount::new(Quantity::one(), PRICE_VALUE); + config_mocks(amount.balance().unwrap()); + + let repay_amount = RepaidInput { + principal: PrincipalInput::External(amount), + interest: 0, + unscheduled: 0, + }; + + let current_price = || { + ActiveLoanInfo::try_from((POOL_A, util::get_loan(loan_id))) + .unwrap() + .current_price + .unwrap() + }; + + // Repay and check time without advance time + assert_ok!(Loans::repay( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + repay_amount.clone() + )); + assert_eq!(current_price(), PRICE_VALUE); + + // In the middle of the line + advance_time(YEAR / 2); + assert_eq!(current_price(), PRICE_VALUE + (NOTIONAL - PRICE_VALUE) / 2); + + // BEFORE: the loan not yet overdue + advance_time(YEAR / 2 - DAY); + assert_ok!(Loans::repay( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + repay_amount.clone() + )); + assert!(current_price() < NOTIONAL); + + // EXACT: the loan is just at matuyrity date + advance_time(DAY); + + assert_ok!(Loans::repay( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + repay_amount.clone() + )); + assert_eq!(current_price(), NOTIONAL); + + // AFTER: the loan overpassing maturity date + advance_time(DAY); + + assert_ok!(Loans::repay( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + repay_amount.clone() + )); + assert_eq!(current_price(), NOTIONAL); + }); +} diff --git a/pallets/loans/src/tests/util.rs b/pallets/loans/src/tests/util.rs index 086d4c6a3a..c12c180bdc 100644 --- a/pallets/loans/src/tests/util.rs +++ b/pallets/loans/src/tests/util.rs @@ -81,6 +81,28 @@ pub fn base_internal_pricing() -> InternalPricing { } } +pub fn dcf_internal_pricing() -> InternalPricing { + InternalPricing { + collateral_value: COLLATERAL_VALUE, + max_borrow_amount: util::total_borrowed_rate(1.0), + valuation_method: ValuationMethod::DiscountedCashFlow(DiscountedCashFlow { + probability_of_default: Rate::from_float(DEFAULT_PROBABILITY_OF_DEFAULT), + loss_given_default: Rate::from_float(DEFAULT_LOSS_GIVEN_DEFAULT), + discount_rate: InterestRate::Fixed { + rate_per_year: Rate::from_float(DEFAULT_DISCOUNT_RATE), + compounding: CompoundingSchedule::Secondly, + }, + }), + } +} + +pub fn dcf_internal_loan() -> LoanInfo { + LoanInfo { + pricing: Pricing::Internal(dcf_internal_pricing()), + ..base_internal_loan() + } +} + pub fn base_internal_loan() -> LoanInfo { LoanInfo { schedule: RepaymentSchedule { @@ -110,6 +132,7 @@ pub fn base_external_pricing() -> ExternalPricing { max_borrow_amount: ExtMaxBorrowAmount::Quantity(QUANTITY), notional: NOTIONAL, max_price_variation: MAX_PRICE_VARIATION, + with_linear_pricing: true, } } diff --git a/pallets/loans/src/types/cashflow.rs b/pallets/loans/src/types/cashflow.rs index 8f8a406746..c3b7d61aab 100644 --- a/pallets/loans/src/types/cashflow.rs +++ b/pallets/loans/src/types/cashflow.rs @@ -21,7 +21,7 @@ use sp_runtime::{ EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureFixedPointNumber, EnsureInto, EnsureSub, EnsureSubAssign, }, - ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, FixedU128, + DispatchError, FixedPointNumber, FixedPointOperand, FixedU128, }; use sp_std::{cmp::min, vec, vec::Vec}; @@ -41,6 +41,8 @@ pub enum Maturity { /// Extension in secs, without special permissions extension: Seconds, }, + /// No Maturity date + None, } impl Maturity { @@ -48,24 +50,30 @@ impl Maturity { Self::Fixed { date, extension: 0 } } - pub fn date(&self) -> Seconds { + pub fn date(&self) -> Option { match self { - Maturity::Fixed { date, .. } => *date, + Maturity::Fixed { date, .. } => Some(*date), + Maturity::None => None, } } pub fn is_valid(&self, now: Seconds) -> bool { match self { Maturity::Fixed { date, .. } => *date > now, + Maturity::None => true, } } - pub fn extends(&mut self, value: Seconds) -> Result<(), ArithmeticError> { + pub fn extends(&mut self, value: Seconds) -> Result<(), DispatchError> { match self { Maturity::Fixed { date, extension } => { date.ensure_add_assign(value)?; - extension.ensure_sub_assign(value) + extension.ensure_sub_assign(value)?; + Ok(()) } + Maturity::None => Err(DispatchError::Other( + "No maturity date that could be extended.", + )), } } } @@ -115,21 +123,25 @@ pub struct RepaymentSchedule { impl RepaymentSchedule { pub fn is_valid(&self, now: Seconds) -> Result { - match self.interest_payments { - InterestPayments::None => (), + let valid = match self.interest_payments { + InterestPayments::None => true, InterestPayments::Monthly(_) => { - let start = date::from_seconds(now)?; - let end = date::from_seconds(self.maturity.date())?; - - // We want to avoid creating a loan with a cashflow consuming a lot of computing - // time Maximum 40 years, which means a cashflow list of 40 * 12 elements - if end.year() - start.year() > 40 { - return Ok(false); + match self.maturity.date() { + Some(maturity) => { + let start = date::from_seconds(now)?; + let end = date::from_seconds(maturity)?; + + // We want to avoid creating a loan with a cashflow consuming a lot of + // computing time Maximum 40 years, which means a cashflow list of 40 * 12 + // elements + end.year() - start.year() <= 40 + } + None => false, } } - } + }; - Ok(self.maturity.is_valid(now)) + Ok(valid && self.maturity.is_valid(now)) } pub fn generate_cashflows( @@ -142,8 +154,12 @@ impl RepaymentSchedule { Balance: FixedPointOperand + EnsureAdd + EnsureDiv, Rate: FixedPointNumber + EnsureDiv, { + let Some(maturity) = self.maturity.date() else { + return Ok(Vec::new()); + }; + let start_date = date::from_seconds(origination_date)?; - let end_date = date::from_seconds(self.maturity.date())?; + let end_date = date::from_seconds(maturity)?; let (timeflow, periods_per_year) = match &self.interest_payments { InterestPayments::None => (vec![], 1), diff --git a/pallets/pool-fees/src/lib.rs b/pallets/pool-fees/src/lib.rs index 5e1f4f2c0f..f7a167ef85 100644 --- a/pallets/pool-fees/src/lib.rs +++ b/pallets/pool-fees/src/lib.rs @@ -327,16 +327,18 @@ pub mod pallet { bucket: PoolFeeBucket, fee: PoolFeeInfoOf, ) -> DispatchResult { - let who = ensure_signed(origin)?; + let who = ensure_signed_or_root(origin)?; ensure!( T::PoolReserve::pool_exists(pool_id), Error::::PoolNotFound ); - ensure!( - T::IsPoolAdmin::check((who, pool_id)), - Error::::NotPoolAdmin - ); + if let Some(signer) = who { + ensure!( + T::IsPoolAdmin::check((signer, pool_id)), + Error::::NotPoolAdmin + ); + } let fee_id = Self::generate_fee_id()?; T::ChangeGuard::note( @@ -385,11 +387,16 @@ pub mod pallet { #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::remove_fee(T::MaxPoolFeesPerBucket::get()))] pub fn remove_fee(origin: OriginFor, fee_id: T::FeeId) -> DispatchResult { - let who = ensure_signed(origin)?; + let who = ensure_signed_or_root(origin)?; let fee = Self::get_active_fee(fee_id)?; + // ensure!( - matches!(fee.editor, PoolFeeEditor::Account(account) if account == who), + match (fee.editor, who) { + (PoolFeeEditor::Account(editor), Some(signer)) => editor == signer, + (PoolFeeEditor::Root, None) => true, + _ => false, + }, Error::::UnauthorizedEdit ); Self::do_remove_fee(fee_id)?; diff --git a/pallets/pool-fees/src/mock.rs b/pallets/pool-fees/src/mock.rs index 28da32d9b2..780f46668b 100644 --- a/pallets/pool-fees/src/mock.rs +++ b/pallets/pool-fees/src/mock.rs @@ -219,6 +219,16 @@ pub fn default_fixed_fee() -> PoolFeeInfoOf { }) } +pub fn root_editor_fee() -> PoolFeeInfoOf { + PoolFeeInfoOf:: { + destination: DESTINATION, + editor: PoolFeeEditor::Root, + fee_type: PoolFeeType::Fixed { + limit: PoolFeeAmount::ShareOfPortfolioValuation(Rate::saturating_from_rational(1, 10)), + }, + } +} + pub fn default_chargeable_fee() -> PoolFeeInfoOf { new_fee(PoolFeeType::ChargedUpTo { limit: PoolFeeAmount::AmountPerSecond(1), diff --git a/pallets/pool-fees/src/tests.rs b/pallets/pool-fees/src/tests.rs index 06285e0a4e..7a4ea0fbd6 100644 --- a/pallets/pool-fees/src/tests.rs +++ b/pallets/pool-fees/src/tests.rs @@ -16,9 +16,10 @@ mod extrinsics { mod should_work { use super::*; + use crate::mock::{default_chargeable_fee, root_editor_fee}; #[test] - fn propose_new_fee_works() { + fn propose_new_fee_works_signed() { ExtBuilder::default().build().execute_with(|| { let fees = default_fees(); @@ -49,6 +50,29 @@ mod extrinsics { }) } + #[test] + fn propose_new_fee_works_root() { + ExtBuilder::default().build().execute_with(|| { + let fee = default_chargeable_fee(); + + assert_ok!(PoolFees::propose_new_fee( + RuntimeOrigin::root(), + POOL, + BUCKET, + fee.clone() + )); + System::assert_last_event( + Event::::Proposed { + fee_id: 1u64, + pool_id: POOL, + bucket: BUCKET, + fee, + } + .into(), + ); + }) + } + #[test] fn apply_new_fee_works() { ExtBuilder::default().build().execute_with(|| { @@ -73,7 +97,7 @@ mod extrinsics { } #[test] - fn remove_only_fee_works() { + fn remove_single_account_editor_fee_works() { ExtBuilder::default().build().execute_with(|| { add_fees(vec![default_fixed_fee()]); @@ -90,6 +114,24 @@ mod extrinsics { }) } + #[test] + fn remove_single_root_editor_fee_works() { + ExtBuilder::default().build().execute_with(|| { + add_fees(vec![root_editor_fee()]); + + assert_ok!(PoolFees::remove_fee(RuntimeOrigin::root(), 1)); + + System::assert_last_event( + Event::::Removed { + pool_id: POOL, + bucket: BUCKET, + fee_id: 1, + } + .into(), + ); + }) + } + #[test] fn remove_fee_works() { ExtBuilder::default().build().execute_with(|| { @@ -257,6 +299,19 @@ mod extrinsics { }) } + #[test] + fn apply_new_fee_from_root() { + ExtBuilder::default().build().execute_with(|| { + config_change_mocks(&default_fixed_fee()); + + // Cannot be called from root + assert_noop!( + PoolFees::apply_new_fee(RuntimeOrigin::root(), POOL, CHANGE_ID), + DispatchError::BadOrigin + ); + }) + } + #[test] fn apply_new_fee_missing_pool() { ExtBuilder::default().build().execute_with(|| { @@ -280,6 +335,10 @@ mod extrinsics { Error::::UnauthorizedEdit ); } + assert_noop!( + PoolFees::remove_fee(RuntimeOrigin::root(), 1), + Error::::UnauthorizedEdit + ); }) } @@ -313,6 +372,10 @@ mod extrinsics { PoolFees::charge_fee(RuntimeOrigin::signed(account), 1, 1000), Error::::UnauthorizedCharge ); + assert_noop!( + PoolFees::charge_fee(RuntimeOrigin::root(), 1, 1000), + DispatchError::BadOrigin + ); } }) } @@ -364,6 +427,10 @@ mod extrinsics { PoolFees::uncharge_fee(RuntimeOrigin::signed(account), 1, 1000), Error::::UnauthorizedCharge ); + assert_noop!( + PoolFees::uncharge_fee(RuntimeOrigin::root(), 1, 1000), + DispatchError::BadOrigin + ); } }) } diff --git a/pallets/pool-registry/src/mock.rs b/pallets/pool-registry/src/mock.rs index 895ff43d84..dd8915028a 100644 --- a/pallets/pool-registry/src/mock.rs +++ b/pallets/pool-registry/src/mock.rs @@ -30,10 +30,10 @@ use cfg_types::{ }; use frame_support::{ derive_impl, - dispatch::DispatchResult, + dispatch::{DispatchResult, RawOrigin}, pallet_prelude::DispatchError, parameter_types, - traits::{Contains, Hooks, PalletInfoAccess, SortedMembers}, + traits::{Contains, EnsureOriginWithArg, Hooks, PalletInfoAccess, SortedMembers}, PalletId, }; use frame_system::EnsureSigned; @@ -115,7 +115,22 @@ impl cfg_test_utils::mocks::nav::Config for Test { type PoolId = PoolId; } +pub struct All; +impl EnsureOriginWithArg for All { + type Success = (); + + fn try_origin(_: RuntimeOrigin, _: &PoolId) -> Result { + Ok(()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin(_: &PoolId) -> Result { + Ok(RawOrigin::Root.into()) + } +} + impl pallet_pool_system::Config for Test { + type AdminOrigin = All; type AssetRegistry = RegistryMock; type AssetsUnderManagementNAV = FakeNav; type Balance = Balance; diff --git a/pallets/pool-system/src/benchmarking.rs b/pallets/pool-system/src/benchmarking.rs index 96996f3036..086a24cd45 100644 --- a/pallets/pool-system/src/benchmarking.rs +++ b/pallets/pool-system/src/benchmarking.rs @@ -73,12 +73,10 @@ benchmarks! { let m in 0..T::PoolFees::get_max_fees_per_bucket(); let admin: T::AccountId = create_admin::(0); - let caller: T::AccountId = create_admin::(1); let max_reserve = MAX_RESERVE / 2; prepare_asset_registry::(); create_pool::(1, m, admin.clone())?; - set_liquidity_admin::(caller.clone())?; - }: set_max_reserve(RawOrigin::Signed(caller), POOL, max_reserve) + }: set_max_reserve(RawOrigin::Signed(admin), POOL, max_reserve) verify { assert_eq!(get_pool::().reserve.max, max_reserve); } @@ -125,9 +123,9 @@ benchmarks! { let admin: T::AccountId = create_admin::(0); prepare_asset_registry::(); create_pool::(n, m, admin.clone())?; - T::AssetsUnderManagementNAV::initialise(RawOrigin::Signed(admin.clone()).into(), POOL, 0.into())?; unrestrict_epoch_close::(); + let investment = MAX_RESERVE / 2; let investor = create_investor::(0, TRANCHE, None)?; let origin = RawOrigin::Signed(investor.clone()).into(); @@ -176,7 +174,6 @@ benchmarks! { let admin: T::AccountId = create_admin::(0); prepare_asset_registry::(); create_pool::(n, m, admin.clone())?; - T::AssetsUnderManagementNAV::initialise(RawOrigin::Signed(admin.clone()).into(), POOL, 0.into())?; unrestrict_epoch_close::(); @@ -293,13 +290,14 @@ where fn set_liquidity_admin>(target: T::AccountId) -> DispatchResult where - T::Permission: Permissions, + T::Permission: Permissions, { T::Permission::add( PermissionScope::Pool(POOL), target, Role::PoolRole(PoolRole::LiquidityAdmin), ) + .map(|_| ()) } pub fn create_pool(num_tranches: u32, num_pool_fees: u32, caller: T::AccountId) -> DispatchResult @@ -314,7 +312,7 @@ where let tranches = build_bench_input_tranches::(num_tranches); Pallet::::create( caller.clone(), - caller, + caller.clone(), POOL, tranches, AUSD_CURRENCY_ID, @@ -323,7 +321,8 @@ where .into_iter() .map(|fee| (PoolFeeBucket::Top, fee)) .collect(), - ) + )?; + set_liquidity_admin::(caller) } pub fn update_pool>( diff --git a/pallets/pool-system/src/lib.rs b/pallets/pool-system/src/lib.rs index 7a949ed3bc..cf4bba7567 100644 --- a/pallets/pool-system/src/lib.rs +++ b/pallets/pool-system/src/lib.rs @@ -184,7 +184,7 @@ pub mod pallet { use frame_support::{ pallet_prelude::*, sp_runtime::traits::Convert, - traits::{tokens::Preservation, Contains}, + traits::{tokens::Preservation, Contains, EnsureOriginWithArg}, PalletId, }; use sp_runtime::{traits::BadOrigin, ArithmeticError}; @@ -195,6 +195,8 @@ pub mod pallet { pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type AdminOrigin: EnsureOriginWithArg; + type Balance: Member + Parameter + AtLeast32BitUnsigned @@ -613,7 +615,7 @@ pub mod pallet { #[transactional] #[pallet::call_index(1)] pub fn close_epoch(origin: OriginFor, pool_id: T::PoolId) -> DispatchResultWithPostInfo { - ensure_signed(origin)?; + T::AdminOrigin::ensure_origin(origin, &pool_id)?; Pool::::try_mutate(pool_id, |pool| { let pool = pool.as_mut().ok_or(Error::::NoSuchPool)?; @@ -876,7 +878,7 @@ pub mod pallet { origin: OriginFor, pool_id: T::PoolId, ) -> DispatchResultWithPostInfo { - ensure_signed(origin)?; + T::AdminOrigin::ensure_origin(origin, &pool_id)?; EpochExecution::::try_mutate(pool_id, |epoch_info| { let epoch = epoch_info diff --git a/pallets/pool-system/src/mock.rs b/pallets/pool-system/src/mock.rs index c1d18709fc..6955679adb 100644 --- a/pallets/pool-system/src/mock.rs +++ b/pallets/pool-system/src/mock.rs @@ -27,8 +27,10 @@ use cfg_types::{ tokens::{CurrencyId, CustomMetadata, TrancheCurrency}, }; use frame_support::{ - assert_ok, derive_impl, parameter_types, - traits::{Contains, Hooks, PalletInfoAccess, SortedMembers}, + assert_ok, derive_impl, + dispatch::RawOrigin, + parameter_types, + traits::{Contains, EnsureOriginWithArg, Hooks, PalletInfoAccess, SortedMembers}, Blake2_128, PalletId, StorageHasher, }; use frame_system::{EnsureSigned, EnsureSignedBy}; @@ -37,11 +39,8 @@ use pallet_pool_fees::PoolFeeInfoOf; use pallet_restricted_tokens::TransferDetails; use parity_scale_codec::Encode; use sp_arithmetic::FixedPointNumber; -use sp_core::H256; -use sp_runtime::{ - traits::{ConstU128, Zero}, - BuildStorage, -}; +use sp_core::{ConstU128, H256}; +use sp_runtime::{traits::Zero, BuildStorage}; use sp_std::marker::PhantomData; use crate::{ @@ -385,7 +384,22 @@ parameter_types! { pub const PoolDeposit: Balance = 1 * CURRENCY; } +pub struct All; +impl EnsureOriginWithArg for All { + type Success = (); + + fn try_origin(_: RuntimeOrigin, _: &PoolId) -> Result { + Ok(()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin(_: &PoolId) -> Result { + Ok(RawOrigin::Root.into()) + } +} + impl Config for Runtime { + type AdminOrigin = All; type AssetRegistry = RegistryMock; type AssetsUnderManagementNAV = FakeNav; type Balance = Balance; diff --git a/pallets/pool-system/src/tests/mod.rs b/pallets/pool-system/src/tests/mod.rs index 6f25a25a24..077dd0e501 100644 --- a/pallets/pool-system/src/tests/mod.rs +++ b/pallets/pool-system/src/tests/mod.rs @@ -506,7 +506,7 @@ fn pool_constraints_pass() { #[test] fn epoch() { new_test_ext().execute_with(|| { - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); let borrower = 3; @@ -744,7 +744,7 @@ fn epoch() { #[test] fn submission_period() { new_test_ext().execute_with(|| { - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); // Initialize pool with initial investments @@ -932,7 +932,7 @@ fn submission_period() { #[test] fn execute_info_removed_after_epoch_execute() { new_test_ext().execute_with(|| { - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); // Initialize pool with initial investments @@ -1021,7 +1021,7 @@ fn execute_info_removed_after_epoch_execute() { #[test] fn pool_updates_should_be_constrained() { new_test_ext().execute_with(|| { - let pool_owner = 0_u64; + let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); let pool_id = 0; @@ -1556,7 +1556,7 @@ fn valid_tranche_structure_is_enforced() { #[test] fn triger_challange_period_with_zero_solution() { new_test_ext().execute_with(|| { - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); // Initialize pool with initial investments @@ -1650,7 +1650,7 @@ fn triger_challange_period_with_zero_solution() { #[test] fn min_challenge_time_is_respected() { new_test_ext().execute_with(|| { - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); // Initialize pool with initial investments @@ -1747,7 +1747,7 @@ fn min_challenge_time_is_respected() { #[test] fn only_zero_solution_is_accepted_max_reserve_violated() { new_test_ext().execute_with(|| { - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); // Initialize pool with initial investments @@ -1948,7 +1948,7 @@ fn only_zero_solution_is_accepted_max_reserve_violated() { #[test] fn only_zero_solution_is_accepted_when_risk_buff_violated_else() { new_test_ext().execute_with(|| { - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; let pool_owner_origin = RuntimeOrigin::signed(pool_owner); // Initialize pool with initial investments @@ -2138,7 +2138,7 @@ fn only_zero_solution_is_accepted_when_risk_buff_violated_else() { #[test] fn only_usd_as_pool_currency_allowed() { new_test_ext().execute_with(|| { - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; // Initialize pool with initial investments let senior_interest_rate = Rate::saturating_from_rational(10, 100) @@ -2331,7 +2331,7 @@ fn creation_takes_deposit() { // Pool creation one: // Owner 2, first deposit // total deposit for this owner is 1 - let pool_owner = 2_u64; + let pool_owner = DEFAULT_POOL_OWNER; assert_ok!(PoolSystem::create( pool_owner.clone(), @@ -2742,7 +2742,6 @@ mod pool_fees { use super::*; use crate::{mock::default_pool_fees, Event}; - const POOL_OWNER: AccountId = 2; const INVESTMENT_AMOUNT: Balance = DEFAULT_POOL_MAX_RESERVE / 10; const NAV_AMOUNT: Balance = INVESTMENT_AMOUNT / 2 + 2_345_000; const FEE_AMOUNT_FIXED: Balance = NAV_AMOUNT / 10; @@ -2761,8 +2760,8 @@ mod pool_fees { let senior_interest_rate = interest_rate / Rate::saturating_from_integer(SECONDS_PER_YEAR) + One::one(); assert_ok!(PoolSystem::create( - POOL_OWNER, - POOL_OWNER, + DEFAULT_POOL_OWNER, + DEFAULT_POOL_OWNER, DEFAULT_POOL_ID, vec![ TrancheInput { @@ -2872,11 +2871,11 @@ mod pool_fees { INVESTMENT_AMOUNT )); assert_ok!(PoolSystem::close_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), 0 )); assert_ok!(PoolSystem::submit_solution( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID, vec![ TrancheSolution { @@ -2892,7 +2891,7 @@ mod pool_fees { // Execute epoch 1 should reduce reserve due to redemption assert_ok!(PoolSystem::execute_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID )); assert!(!EpochExecution::::contains_key(DEFAULT_POOL_ID)); @@ -2920,7 +2919,7 @@ mod pool_fees { // Closing epoch 2 should not change anything but reserve.available next_block(); assert_ok!(PoolSystem::close_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), 0 )); assert_eq!( @@ -3003,7 +3002,7 @@ mod pool_fees { )); next_block(); assert_ok!(PoolSystem::close_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), 0 )); assert_eq!( @@ -3056,7 +3055,7 @@ mod pool_fees { // Executing epoch should reduce FeeNav by disbursement and transfer from // PoolFees account to destination assert_ok!(PoolSystem::submit_solution( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID, vec![ TrancheSolution { @@ -3070,7 +3069,7 @@ mod pool_fees { ] )); assert_ok!(PoolSystem::execute_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID )); assert!(!EpochExecution::::contains_key(DEFAULT_POOL_ID)); @@ -3105,7 +3104,7 @@ mod pool_fees { next_block(); test_nav_up(DEFAULT_POOL_ID, new_nav_amount - NAV_AMOUNT); assert_ok!(PoolSystem::close_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), 0 )); @@ -3158,7 +3157,7 @@ mod pool_fees { // NAV = 0 + AUM - PoolFeesNAV = -AUM assert_ok!(PoolSystem::close_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), 0 )); assert!(System::events().iter().any(|e| match e.event { @@ -3227,7 +3226,7 @@ mod pool_fees { // Closing should update fee nav next_block(); assert_ok!(PoolSystem::close_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), 0 )); let fee_amount_from_charge = @@ -3259,7 +3258,7 @@ mod pool_fees { // Executin should reduce fee_nav by disbursement and transfer assert_ok!(PoolSystem::submit_solution( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID, vec![ TrancheSolution { @@ -3273,7 +3272,7 @@ mod pool_fees { ] )); assert_ok!(PoolSystem::execute_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID )); assert_eq!( @@ -3345,7 +3344,7 @@ mod pool_fees { // Closing should update fee nav next_block(); assert_ok!(PoolSystem::close_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), 0 )); assert_eq!( @@ -3362,7 +3361,7 @@ mod pool_fees { // by fees assert_noop!( PoolSystem::submit_solution( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID, vec![ TrancheSolution { @@ -3378,7 +3377,7 @@ mod pool_fees { Error::::InsufficientCurrency ); assert_ok!(PoolSystem::submit_solution( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID, vec![ TrancheSolution { @@ -3392,7 +3391,7 @@ mod pool_fees { ] )); assert_ok!(PoolSystem::execute_epoch( - RuntimeOrigin::signed(POOL_OWNER), + RuntimeOrigin::signed(DEFAULT_POOL_OWNER), DEFAULT_POOL_ID )); assert_pending_fees(DEFAULT_POOL_ID, fees.clone(), vec![(fee_nav, 0, None)]); diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index 985a29f1d4..1285394429 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -538,30 +538,32 @@ impl InstanceFilter for ProxyType { matches!(c, RuntimeCall::Proxy(pallet_proxy::Call::proxy { .. })) || !matches!(c, RuntimeCall::Proxy(..)) } - ProxyType::Borrow => matches!( - c, - RuntimeCall::Loans(pallet_loans::Call::create { .. }) | - RuntimeCall::Loans(pallet_loans::Call::borrow { .. }) | - RuntimeCall::Loans(pallet_loans::Call::repay { .. }) | - RuntimeCall::Loans(pallet_loans::Call::write_off { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_loan_mutation { .. }) | - RuntimeCall::Loans(pallet_loans::Call::close { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_write_off_policy { .. }) | - RuntimeCall::Loans(pallet_loans::Call::update_portfolio_valuation { .. }) | - RuntimeCall::Loans(pallet_loans::Call::propose_transfer_debt { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_transfer_debt { .. }) | - // Borrowers should be able to close and execute an epoch - // in order to get liquidity from repayments in previous epochs. - RuntimeCall::PoolSystem(pallet_pool_system::Call::close_epoch{..}) | - RuntimeCall::PoolSystem(pallet_pool_system::Call::submit_solution{..}) | - RuntimeCall::PoolSystem(pallet_pool_system::Call::execute_epoch{..}) | - RuntimeCall::Utility(pallet_utility::Call::batch_all{..}) | - RuntimeCall::Utility(pallet_utility::Call::batch{..}) | - // Borrowers should be able to swap back and forth between local currencies and their variants - RuntimeCall::TokenMux(pallet_token_mux::Call::burn {..}) | - RuntimeCall::TokenMux(pallet_token_mux::Call::deposit {..}) | - RuntimeCall::TokenMux(pallet_token_mux::Call::match_swap {..}) - ), + ProxyType::Borrow => { + matches!( + c, + RuntimeCall::Loans(pallet_loans::Call::create { .. }) | + RuntimeCall::Loans(pallet_loans::Call::borrow { .. }) | + RuntimeCall::Loans(pallet_loans::Call::repay { .. }) | + RuntimeCall::Loans(pallet_loans::Call::write_off { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_loan_mutation { .. }) | + RuntimeCall::Loans(pallet_loans::Call::close { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_write_off_policy { .. }) | + RuntimeCall::Loans(pallet_loans::Call::update_portfolio_valuation { .. }) | + RuntimeCall::Loans(pallet_loans::Call::propose_transfer_debt { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_transfer_debt { .. }) | + // Borrowers should be able to close and execute an epoch + // in order to get liquidity from repayments in previous epochs. + RuntimeCall::PoolSystem(pallet_pool_system::Call::close_epoch{..}) | + RuntimeCall::PoolSystem(pallet_pool_system::Call::submit_solution{..}) | + RuntimeCall::PoolSystem(pallet_pool_system::Call::execute_epoch{..}) | + RuntimeCall::Utility(pallet_utility::Call::batch_all{..}) | + RuntimeCall::Utility(pallet_utility::Call::batch{..}) | + // Borrowers should be able to swap back and forth between local currencies and their variants + RuntimeCall::TokenMux(pallet_token_mux::Call::burn {..}) | + RuntimeCall::TokenMux(pallet_token_mux::Call::deposit {..}) | + RuntimeCall::TokenMux(pallet_token_mux::Call::match_swap {..}) + ) | ProxyType::PodOperation.filter(c) + } ProxyType::Invest => matches!( c, RuntimeCall::Investments(pallet_investments::Call::update_invest_order{..}) | @@ -1420,6 +1422,7 @@ parameter_types! { } impl pallet_pool_system::Config for Runtime { + type AdminOrigin = runtime_common::pool::LiquidityAndPoolAdminOrRoot; type AssetRegistry = OrmlAssetRegistry; type AssetsUnderManagementNAV = Loans; type Balance = Balance; diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index 9b5bc0ef34..40533d4815 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -672,30 +672,32 @@ impl InstanceFilter for ProxyType { matches!(c, RuntimeCall::Proxy(pallet_proxy::Call::proxy { .. })) || !matches!(c, RuntimeCall::Proxy(..)) } - ProxyType::Borrow => matches!( - c, - RuntimeCall::Loans(pallet_loans::Call::create { .. }) | - RuntimeCall::Loans(pallet_loans::Call::borrow { .. }) | - RuntimeCall::Loans(pallet_loans::Call::repay { .. }) | - RuntimeCall::Loans(pallet_loans::Call::write_off { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_loan_mutation { .. }) | - RuntimeCall::Loans(pallet_loans::Call::close { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_write_off_policy { .. }) | - RuntimeCall::Loans(pallet_loans::Call::update_portfolio_valuation { .. }) | - RuntimeCall::Loans(pallet_loans::Call::propose_transfer_debt { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_transfer_debt { .. }) | - // Borrowers should be able to close and execute an epoch - // in order to get liquidity from repayments in previous epochs. - RuntimeCall::PoolSystem(pallet_pool_system::Call::close_epoch{..}) | - RuntimeCall::PoolSystem(pallet_pool_system::Call::submit_solution{..}) | - RuntimeCall::PoolSystem(pallet_pool_system::Call::execute_epoch{..}) | - RuntimeCall::Utility(pallet_utility::Call::batch_all{..}) | - RuntimeCall::Utility(pallet_utility::Call::batch{..}) | - // Borrowers should be able to swap back and forth between local currencies and their variants - RuntimeCall::TokenMux(pallet_token_mux::Call::burn {..}) | - RuntimeCall::TokenMux(pallet_token_mux::Call::deposit {..}) | - RuntimeCall::TokenMux(pallet_token_mux::Call::match_swap {..}) - ), + ProxyType::Borrow => { + matches!( + c, + RuntimeCall::Loans(pallet_loans::Call::create { .. }) | + RuntimeCall::Loans(pallet_loans::Call::borrow { .. }) | + RuntimeCall::Loans(pallet_loans::Call::repay { .. }) | + RuntimeCall::Loans(pallet_loans::Call::write_off { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_loan_mutation { .. }) | + RuntimeCall::Loans(pallet_loans::Call::close { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_write_off_policy { .. }) | + RuntimeCall::Loans(pallet_loans::Call::update_portfolio_valuation { .. }) | + RuntimeCall::Loans(pallet_loans::Call::propose_transfer_debt { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_transfer_debt { .. }) | + // Borrowers should be able to close and execute an epoch + // in order to get liquidity from repayments in previous epochs. + RuntimeCall::PoolSystem(pallet_pool_system::Call::close_epoch{..}) | + RuntimeCall::PoolSystem(pallet_pool_system::Call::submit_solution{..}) | + RuntimeCall::PoolSystem(pallet_pool_system::Call::execute_epoch{..}) | + RuntimeCall::Utility(pallet_utility::Call::batch_all{..}) | + RuntimeCall::Utility(pallet_utility::Call::batch{..}) | + // Borrowers should be able to swap back and forth between local currencies and their variants + RuntimeCall::TokenMux(pallet_token_mux::Call::burn {..}) | + RuntimeCall::TokenMux(pallet_token_mux::Call::deposit {..}) | + RuntimeCall::TokenMux(pallet_token_mux::Call::match_swap {..}) + ) | ProxyType::PodOperation.filter(c) + } ProxyType::Invest => matches!( c, RuntimeCall::Investments(pallet_investments::Call::update_invest_order{..}) | @@ -1428,6 +1430,7 @@ impl pallet_pool_registry::Config for Runtime { } impl pallet_pool_system::Config for Runtime { + type AdminOrigin = runtime_common::pool::LiquidityAndPoolAdminOrRoot; type AssetRegistry = OrmlAssetRegistry; type AssetsUnderManagementNAV = Loans; type Balance = Balance; diff --git a/runtime/centrifuge/src/migrations.rs b/runtime/centrifuge/src/migrations.rs index ae20500a12..7bba719806 100644 --- a/runtime/centrifuge/src/migrations.rs +++ b/runtime/centrifuge/src/migrations.rs @@ -18,4 +18,5 @@ pub type UpgradeCentrifuge1029 = ( runtime_common::migrations::increase_storage_version::Migration, runtime_common::migrations::increase_storage_version::Migration, pallet_collator_selection::migration::v1::MigrateToV1, + runtime_common::migrations::loans::AddWithLinearPricing, ); diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 673b2bb54d..ebc4227130 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -23,6 +23,7 @@ pub mod fees; pub mod gateway; pub mod migrations; pub mod oracle; +pub mod pool; pub mod remarks; pub mod transfer_filter; pub mod xcm; @@ -112,12 +113,9 @@ where >, { let input_prices: PriceCollectionInput = - if let Ok(prices) = pallet_loans::Pallet::::registered_prices(pool_id) { - PriceCollectionInput::Custom(prices.try_into().map_err(|_| { - DispatchError::Other("Map expected to fit as it is coming from loans itself.") - })?) - } else { - PriceCollectionInput::Empty + match pallet_loans::Pallet::::registered_prices(pool_id) { + Ok(_) => PriceCollectionInput::FromRegistry, + Err(_) => PriceCollectionInput::Empty, }; update_nav_with_input::(pool_id, input_prices) diff --git a/runtime/common/src/migrations/loans.rs b/runtime/common/src/migrations/loans.rs new file mode 100644 index 0000000000..7291ae00a5 --- /dev/null +++ b/runtime/common/src/migrations/loans.rs @@ -0,0 +1,194 @@ +// Copyright 2024 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_traits::PoolNAV; +#[cfg(feature = "try-runtime")] +use frame_support::pallet_prelude::{Decode, Encode}; +use frame_support::{ + pallet_prelude::StorageVersion, + traits::{Get, GetStorageVersion, OnRuntimeUpgrade}, + weights::Weight, +}; +use pallet_loans::{pallet::Pallet as Loans, Config}; +#[cfg(feature = "try-runtime")] +use sp_arithmetic::traits::SaturatedConversion; +#[cfg(feature = "try-runtime")] +use sp_runtime::TryRuntimeError; +use sp_std::vec::Vec; + +const LOG_PREFIX: &str = "LoansMigrationToV4:"; + +mod v3 { + use frame_support::{ + pallet_prelude::{OptionQuery, ValueQuery}, + storage_alias, Blake2_128Concat, BoundedVec, + }; + pub use pallet_loans::entities::loans::v3::{ + ActiveLoan, ClosedLoan as ClosedLoanV3, CreatedLoan as CreatedLoanV3, + }; + use pallet_loans::Config; + + use super::Loans; + + pub type ActiveLoansVec = + BoundedVec<(::LoanId, ActiveLoan), ::MaxActiveLoansPerPool>; + + #[storage_alias] + pub type ActiveLoans = StorageMap< + Loans, + Blake2_128Concat, + ::PoolId, + ActiveLoansVec, + ValueQuery, + >; + + #[storage_alias] + pub type CreatedLoan = StorageDoubleMap< + Loans, + Blake2_128Concat, + ::PoolId, + Blake2_128Concat, + ::LoanId, + CreatedLoanV3, + OptionQuery, + >; + + #[storage_alias] + pub type ClosedLoan = StorageDoubleMap< + Loans, + Blake2_128Concat, + ::PoolId, + Blake2_128Concat, + ::LoanId, + ClosedLoanV3, + OptionQuery, + >; +} + +pub struct AddWithLinearPricing(sp_std::marker::PhantomData); +impl OnRuntimeUpgrade for AddWithLinearPricing +where + T: Config, +{ + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + assert_eq!( + Loans::::on_chain_storage_version(), + StorageVersion::new(3) + ); + + let created_loans: u64 = v3::CreatedLoan::::iter_keys().count().saturated_into(); + let closed_loans: u64 = v3::ClosedLoan::::iter_keys().count().saturated_into(); + let active_loans: u64 = v3::ActiveLoans::::iter_values() + .map(|v| v.len()) + .sum::() + .saturated_into(); + + log::info!("{LOG_PREFIX} Pre checks done!"); + + Ok((created_loans, active_loans, closed_loans).encode()) + } + + fn on_runtime_upgrade() -> Weight { + let mut weight = T::DbWeight::get().reads(1); + if Loans::::on_chain_storage_version() == StorageVersion::new(3) { + log::info!("{LOG_PREFIX} Starting migration v3 -> v4"); + + pallet_loans::CreatedLoan::::translate::, _>(|_, _, loan| { + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + Some(loan.migrate(true)) + }); + + pallet_loans::ClosedLoan::::translate::, _>(|_, _, loan| { + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + Some(loan.migrate(true)) + }); + + let mut changed_pools = Vec::new(); + pallet_loans::ActiveLoans::::translate::, _>( + |pool_id, active_loans| { + changed_pools.push(pool_id); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + Some( + active_loans + .into_iter() + .map(|(loan_id, active_loan)| (loan_id, active_loan.migrate(true))) + .collect::>() + .try_into() + .expect("size doesn't not change, qed"), + ) + }, + ); + + for pool_id in &changed_pools { + match Loans::::update_nav(*pool_id) { + Ok(_) => log::info!("{LOG_PREFIX} updated portfolio for pool_id: {pool_id:?}"), + Err(e) => { + log::error!( + "{LOG_PREFIX} error updating the portfolio for {pool_id:?}: {e:?}" + ) + } + } + } + + Loans::::current_storage_version().put::>(); + + let count = changed_pools.len() as u64; + weight.saturating_accrue(T::DbWeight::get().reads_writes(count, count + 1)); + log::info!("{LOG_PREFIX} Migrated {} pools", count); + + weight + } else { + log::info!( + "{LOG_PREFIX} Migration to v4 did not execute. This probably should be removed" + ); + weight + } + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(pre_state: Vec) -> Result<(), TryRuntimeError> { + assert_eq!( + Loans::::on_chain_storage_version(), + StorageVersion::new(4) + ); + + let (pre_created, pre_active, pre_closed) = + <(u64, u64, u64)>::decode(&mut pre_state.as_slice()).expect("Pre state valid; qed"); + let post_created: u64 = pallet_loans::CreatedLoan::::iter_keys() + .count() + .saturated_into(); + let post_closed: u64 = pallet_loans::ClosedLoan::::iter_keys() + .count() + .saturated_into(); + let post_active: u64 = pallet_loans::ActiveLoans::::iter_values() + .map(|v| v.len()) + .sum::() + .saturated_into(); + assert_eq!( + pre_created, post_created, + "Number of CreatedLoans mismatches: pre {pre_created} vs post {post_created}" + ); + assert_eq!( + pre_closed, post_closed, + "Number of ClosedLoans mismatches: pre {pre_closed} vs post {post_closed}" + ); + assert_eq!( + pre_active, post_active, + "Number of ActiveLoans mismatches: pre {pre_active} vs post {post_active}" + ); + + log::info!("{LOG_PREFIX} Post checks done!"); + + Ok(()) + } +} diff --git a/runtime/common/src/migrations/mod.rs b/runtime/common/src/migrations/mod.rs index 8e8cee67f5..b4bbaa8069 100644 --- a/runtime/common/src/migrations/mod.rs +++ b/runtime/common/src/migrations/mod.rs @@ -13,6 +13,7 @@ //! Centrifuge Runtime-Common Migrations pub mod increase_storage_version; +pub mod loans; pub mod nuke; pub mod precompile_account_codes; diff --git a/runtime/common/src/pool.rs b/runtime/common/src/pool.rs new file mode 100644 index 0000000000..6ab2c8ccb9 --- /dev/null +++ b/runtime/common/src/pool.rs @@ -0,0 +1,59 @@ +// Copyright 2021 Centrifuge Foundation (centrifuge.io). +// This file is part of Centrifuge chain project. + +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). + +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_traits::Permissions; +use cfg_types::permissions::{PermissionScope, PoolRole, Role}; +use frame_support::{dispatch::RawOrigin, traits::EnsureOriginWithArg}; + +pub struct LiquidityAndPoolAdminOrRoot(sp_std::marker::PhantomData); + +impl< + T: frame_system::Config + + pallet_permissions::Config< + Scope = PermissionScope, + Role = Role, + > + pallet_pool_system::Config, + > EnsureOriginWithArg for LiquidityAndPoolAdminOrRoot +{ + type Success = (); + + fn try_origin( + o: T::RuntimeOrigin, + pool_id: &T::PoolId, + ) -> Result { + o.into().and_then(|r| match r { + RawOrigin::Root => Ok(()), + RawOrigin::Signed(by) => { + if as Permissions>::has( + PermissionScope::Pool(*pool_id), + by.clone(), + Role::PoolRole(PoolRole::PoolAdmin), + ) || as Permissions>::has( + PermissionScope::Pool(*pool_id), + by.clone(), + Role::PoolRole(PoolRole::LiquidityAdmin), + ) { + Ok(()) + } else { + Err(RawOrigin::Signed(by).into()) + } + } + RawOrigin::None => Err(RawOrigin::None.into()), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin(_: &T::PoolId) -> Result { + Ok(RawOrigin::Root.into()) + } +} diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index 4e74004e57..d73b0c1262 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -546,30 +546,32 @@ impl InstanceFilter for ProxyType { matches!(c, RuntimeCall::Proxy(pallet_proxy::Call::proxy { .. })) || !matches!(c, RuntimeCall::Proxy(..)) } - ProxyType::Borrow => matches!( - c, - RuntimeCall::Loans(pallet_loans::Call::create { .. }) | - RuntimeCall::Loans(pallet_loans::Call::borrow { .. }) | - RuntimeCall::Loans(pallet_loans::Call::repay { .. }) | - RuntimeCall::Loans(pallet_loans::Call::write_off { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_loan_mutation { .. }) | - RuntimeCall::Loans(pallet_loans::Call::close { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_write_off_policy { .. }) | - RuntimeCall::Loans(pallet_loans::Call::update_portfolio_valuation { .. }) | - RuntimeCall::Loans(pallet_loans::Call::propose_transfer_debt { .. }) | - RuntimeCall::Loans(pallet_loans::Call::apply_transfer_debt { .. }) | - // Borrowers should be able to close and execute an epoch - // in order to get liquidity from repayments in previous epochs. - RuntimeCall::PoolSystem(pallet_pool_system::Call::close_epoch { .. }) | - RuntimeCall::PoolSystem(pallet_pool_system::Call::submit_solution { .. }) | - RuntimeCall::PoolSystem(pallet_pool_system::Call::execute_epoch { .. }) | - RuntimeCall::Utility(pallet_utility::Call::batch_all { .. }) | - RuntimeCall::Utility(pallet_utility::Call::batch { .. }) | - // Borrowers should be able to swap back and forth between local currencies and their variants - RuntimeCall::TokenMux(pallet_token_mux::Call::burn {..}) | - RuntimeCall::TokenMux(pallet_token_mux::Call::deposit {..}) | - RuntimeCall::TokenMux(pallet_token_mux::Call::match_swap {..}) - ), + ProxyType::Borrow => { + matches!( + c, + RuntimeCall::Loans(pallet_loans::Call::create { .. }) | + RuntimeCall::Loans(pallet_loans::Call::borrow { .. }) | + RuntimeCall::Loans(pallet_loans::Call::repay { .. }) | + RuntimeCall::Loans(pallet_loans::Call::write_off { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_loan_mutation { .. }) | + RuntimeCall::Loans(pallet_loans::Call::close { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_write_off_policy { .. }) | + RuntimeCall::Loans(pallet_loans::Call::update_portfolio_valuation { .. }) | + RuntimeCall::Loans(pallet_loans::Call::propose_transfer_debt { .. }) | + RuntimeCall::Loans(pallet_loans::Call::apply_transfer_debt { .. }) | + // Borrowers should be able to close and execute an epoch + // in order to get liquidity from repayments in previous epochs. + RuntimeCall::PoolSystem(pallet_pool_system::Call::close_epoch { .. }) | + RuntimeCall::PoolSystem(pallet_pool_system::Call::submit_solution { .. }) | + RuntimeCall::PoolSystem(pallet_pool_system::Call::execute_epoch { .. }) | + RuntimeCall::Utility(pallet_utility::Call::batch_all { .. }) | + RuntimeCall::Utility(pallet_utility::Call::batch { .. }) | + // Borrowers should be able to swap back and forth between local currencies and their variants + RuntimeCall::TokenMux(pallet_token_mux::Call::burn {..}) | + RuntimeCall::TokenMux(pallet_token_mux::Call::deposit {..}) | + RuntimeCall::TokenMux(pallet_token_mux::Call::match_swap {..}) + ) | ProxyType::PodOperation.filter(c) + } ProxyType::Invest => matches!( c, RuntimeCall::Investments(pallet_investments::Call::update_invest_order{..}) | @@ -1037,6 +1039,7 @@ parameter_types! { } impl pallet_pool_system::Config for Runtime { + type AdminOrigin = runtime_common::pool::LiquidityAndPoolAdminOrRoot; type AssetRegistry = OrmlAssetRegistry; type AssetsUnderManagementNAV = Loans; type Balance = Balance; diff --git a/runtime/development/src/migrations.rs b/runtime/development/src/migrations.rs index 105b1ac17e..0f3d1912b6 100644 --- a/runtime/development/src/migrations.rs +++ b/runtime/development/src/migrations.rs @@ -49,6 +49,7 @@ pub type UpgradeDevelopment1047 = ( CollatorReward, AnnualTreasuryInflationPercent, >, + runtime_common::migrations::loans::AddWithLinearPricing, ); mod cleanup_foreign_investments { diff --git a/runtime/integration-tests/procedural/src/lib.rs b/runtime/integration-tests/procedural/src/lib.rs index 5eb07d3698..da8252ffd9 100644 --- a/runtime/integration-tests/procedural/src/lib.rs +++ b/runtime/integration-tests/procedural/src/lib.rs @@ -52,7 +52,7 @@ pub fn test_runtimes(args: TokenStream, input: TokenStream) -> TokenStream { let func_name = &func.sig.ident; quote! { - crate::test_for_runtimes!(#args, #func_name); + crate::__test_for_runtimes!(#args, #func_name); #func } .into() diff --git a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs index 32ea52b414..e403b70ed9 100644 --- a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs +++ b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs @@ -867,6 +867,7 @@ mod development { use super::*; + #[test_runtimes([development])] fn add_pool() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -916,6 +917,7 @@ mod development { }); } + #[test_runtimes([development])] fn add_tranche() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -987,6 +989,7 @@ mod development { }); } + #[test_runtimes([development])] fn update_member() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1071,6 +1074,7 @@ mod development { }); } + #[test_runtimes([development])] fn update_token_price() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1102,6 +1106,7 @@ mod development { }); } + #[test_runtimes([development])] fn add_currency() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1176,6 +1181,7 @@ mod development { .expect("expected RouterExecutionSuccess event"); } + #[test_runtimes([development])] fn add_currency_should_fail() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1305,6 +1311,7 @@ mod development { }); } + #[test_runtimes([development])] fn allow_investment_currency() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1369,6 +1376,7 @@ mod development { }); } + #[test_runtimes([development])] fn allow_investment_currency_should_fail() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1498,6 +1506,7 @@ mod development { }); } + #[test_runtimes([development])] fn disallow_investment_currency() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1562,6 +1571,7 @@ mod development { }); } + #[test_runtimes([development])] fn disallow_investment_currency_should_fail() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1691,6 +1701,7 @@ mod development { }); } + #[test_runtimes([development])] fn schedule_upgrade() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1724,6 +1735,7 @@ mod development { }); } + #[test_runtimes([development])] fn cancel_upgrade() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1757,6 +1769,7 @@ mod development { }); } + #[test_runtimes([development])] fn update_tranche_token_metadata() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1816,20 +1829,6 @@ mod development { ); }); } - - crate::test_for_runtimes!([development], add_pool); - crate::test_for_runtimes!([development], add_tranche); - crate::test_for_runtimes!([development], update_member); - crate::test_for_runtimes!([development], update_token_price); - crate::test_for_runtimes!([development], add_currency); - crate::test_for_runtimes!([development], add_currency_should_fail); - crate::test_for_runtimes!([development], allow_investment_currency); - crate::test_for_runtimes!([development], allow_investment_currency_should_fail); - crate::test_for_runtimes!([development], disallow_investment_currency); - crate::test_for_runtimes!([development], disallow_investment_currency_should_fail); - crate::test_for_runtimes!([development], schedule_upgrade); - crate::test_for_runtimes!([development], cancel_upgrade); - crate::test_for_runtimes!([development], update_tranche_token_metadata); } mod foreign_investments { @@ -1838,6 +1837,7 @@ mod development { mod same_currencies { use super::*; + #[test_runtimes([development])] fn increase_invest_order() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1894,6 +1894,7 @@ mod development { }); } + #[test_runtimes([development])] fn decrease_invest_order() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -1986,6 +1987,7 @@ mod development { }); } + #[test_runtimes([development])] fn cancel_invest_order() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -2085,6 +2087,7 @@ mod development { }); } + #[test_runtimes([development])] fn collect_invest_order() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -2236,6 +2239,7 @@ mod development { }); } + #[test_runtimes([development])] fn partially_collect_investment_for_through_investments() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -2468,6 +2472,7 @@ mod development { }); } + #[test_runtimes([development])] fn increase_redeem_order() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -2525,6 +2530,7 @@ mod development { }); } + #[test_runtimes([development])] fn decrease_redeem_order() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -2644,6 +2650,7 @@ mod development { }); } + #[test_runtimes([development])] fn cancel_redeem_order() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -2741,6 +2748,7 @@ mod development { }); } + #[test_runtimes([development])] fn fully_collect_redeem_order() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -2895,6 +2903,7 @@ mod development { }); } + #[test_runtimes([development])] fn partially_collect_redemption_for_through_investments() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3098,29 +3107,13 @@ mod development { }); } - crate::test_for_runtimes!([development], increase_invest_order); - crate::test_for_runtimes!([development], decrease_invest_order); - crate::test_for_runtimes!([development], cancel_invest_order); - crate::test_for_runtimes!([development], collect_invest_order); - crate::test_for_runtimes!( - [development], - partially_collect_investment_for_through_investments - ); - crate::test_for_runtimes!([development], increase_redeem_order); - crate::test_for_runtimes!([development], decrease_redeem_order); - crate::test_for_runtimes!([development], cancel_redeem_order); - crate::test_for_runtimes!([development], fully_collect_redeem_order); - crate::test_for_runtimes!( - [development], - partially_collect_redemption_for_through_investments - ); - mod should_fail { use super::*; mod decrease_should_underflow { use super::*; + #[test_runtimes([development])] fn invest_decrease_underflow() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3170,6 +3163,7 @@ mod development { }); } + #[test_runtimes([development])] fn redeem_decrease_underflow() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3217,14 +3211,12 @@ mod development { ); }); } - - crate::test_for_runtimes!([development], invest_decrease_underflow); - crate::test_for_runtimes!([development], redeem_decrease_underflow); } mod should_throw_requires_collect { use super::*; + #[test_runtimes([development])] fn invest_requires_collect() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3308,6 +3300,7 @@ mod development { }); } + #[test_runtimes([development])] fn redeem_requires_collect() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3397,14 +3390,12 @@ mod development { ); }); } - - crate::test_for_runtimes!([development], invest_requires_collect); - crate::test_for_runtimes!([development], redeem_requires_collect); } mod payment_payout_currency { use super::*; + #[test_runtimes([development])] fn invalid_invest_payment_currency() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3484,6 +3475,7 @@ mod development { }); } + #[test_runtimes([development])] fn invalid_redeem_payout_currency() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3567,6 +3559,7 @@ mod development { }); } + #[test_runtimes([development])] fn redeem_payout_currency_not_found() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3636,10 +3629,6 @@ mod development { ); }); } - - crate::test_for_runtimes!([development], invalid_invest_payment_currency); - crate::test_for_runtimes!([development], invalid_redeem_payout_currency); - crate::test_for_runtimes!([development], redeem_payout_currency_not_found); } } } @@ -3647,6 +3636,7 @@ mod development { mod mismatching_currencies { use super::*; + #[test_runtimes([development])] fn collect_foreign_investment_for() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3764,6 +3754,7 @@ mod development { /// Invest in pool currency, then increase in allowed foreign /// currency, then decrease in same foreign currency multiple times. + #[test_runtimes([development])] fn increase_fulfill_increase_decrease_decrease_partial() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -3902,6 +3893,7 @@ mod development { /// Propagate swaps only via OrderBook fulfillments. /// /// Flow: Increase, fulfill, decrease, fulfill + #[test_runtimes([development])] fn invest_swaps_happy_path() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4024,6 +4016,7 @@ mod development { }); } + #[test_runtimes([development])] fn increase_fulfill_decrease_fulfill_partial_increase() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4123,23 +4116,13 @@ mod development { })); }); } - - crate::test_for_runtimes!([development], collect_foreign_investment_for); - crate::test_for_runtimes!( - [development], - increase_fulfill_increase_decrease_decrease_partial - ); - crate::test_for_runtimes!( - [development], - increase_fulfill_decrease_fulfill_partial_increase - ); - crate::test_for_runtimes!([development], invest_swaps_happy_path); } } mod transfers { use super::*; + #[test_runtimes([development])] fn transfer_non_tranche_tokens_from_local() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4245,6 +4228,7 @@ mod development { }); } + #[test_runtimes([development])] fn transfer_non_tranche_tokens_to_local() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4303,6 +4287,7 @@ mod development { }); } + #[test_runtimes([development])] fn transfer_tranche_tokens_from_local() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4403,6 +4388,7 @@ mod development { }); } + #[test_runtimes([development])] fn transfer_tranche_tokens_to_local() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4493,6 +4479,7 @@ mod development { /// Try to transfer tranches for non-existing pools or invalid tranche /// ids for existing pools. + #[test_runtimes([development])] fn transferring_invalid_tranche_tokens_should_fail() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4667,6 +4654,7 @@ mod development { }); } + #[test_runtimes([development])] fn transfer_cfg_to_and_from_sibling() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4752,16 +4740,6 @@ mod development { ); }); } - - crate::test_for_runtimes!([development], transfer_non_tranche_tokens_from_local); - crate::test_for_runtimes!([development], transfer_non_tranche_tokens_to_local); - crate::test_for_runtimes!([development], transfer_tranche_tokens_from_local); - crate::test_for_runtimes!([development], transfer_tranche_tokens_to_local); - crate::test_for_runtimes!( - [development], - transferring_invalid_tranche_tokens_should_fail - ); - crate::test_for_runtimes!([development], transfer_cfg_to_and_from_sibling); } mod routers { @@ -4772,6 +4750,7 @@ mod development { use super::*; + #[test_runtimes([development])] fn test_via_outbound_queue() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -4925,8 +4904,6 @@ mod development { ); }); } - - crate::test_for_runtimes!([development], test_via_outbound_queue); } mod ethereum_xcm { @@ -5079,22 +5056,22 @@ mod development { const TEST_DOMAIN: Domain = Domain::EVM(1); + #[test_runtimes([development])] fn submit_ethereum_xcm() { submit_test_fn::(get_ethereum_xcm_router_fn::()); } + #[test_runtimes([development])] fn submit_axelar_xcm() { submit_test_fn::(get_axelar_xcm_router_fn::()); } - - crate::test_for_runtimes!([development], submit_ethereum_xcm); - crate::test_for_runtimes!([development], submit_axelar_xcm); } } mod gateway { use super::*; + #[test_runtimes([development])] fn set_domain_router() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -5164,6 +5141,7 @@ mod development { }); } + #[test_runtimes([development])] fn add_remove_instances() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -5220,6 +5198,7 @@ mod development { }); } + #[test_runtimes([development])] fn process_msg() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -5274,10 +5253,6 @@ mod development { ); }); } - - crate::test_for_runtimes!([development], set_domain_router); - crate::test_for_runtimes!([development], add_remove_instances); - crate::test_for_runtimes!([development], process_msg); } } @@ -5489,6 +5464,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn test_air_transfers_to_and_from_sibling() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -5567,6 +5543,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn transfer_ausd_to_altair() { let mut env = FudgeEnv::::default(); @@ -5726,6 +5703,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn transfer_ksm_to_and_from_relay_chain() { let mut env = FudgeEnv::::default(); @@ -5785,6 +5763,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn transfer_foreign_sibling_to_altair() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -5892,6 +5871,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn transfer_wormhole_usdc_karura_to_altair() { let mut env = FudgeEnv::::from_storage( Default::default(), @@ -5993,17 +5973,12 @@ mod altair { assert_eq!(bob_balance, 11993571); }); } - - crate::test_for_runtimes!([altair], test_air_transfers_to_and_from_sibling); - crate::test_for_runtimes!([altair], transfer_ausd_to_altair); - crate::test_for_runtimes!([altair], transfer_ksm_to_and_from_relay_chain); - crate::test_for_runtimes!([altair], transfer_foreign_sibling_to_altair); - crate::test_for_runtimes!([altair], transfer_wormhole_usdc_karura_to_altair); } mod asset_registry { use super::*; + #[test_runtimes([altair])] fn register_air_works() { let mut env = FudgeEnv::::default(); @@ -6031,6 +6006,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn register_foreign_asset_works() { let mut env = FudgeEnv::::default(); @@ -6062,6 +6038,7 @@ mod altair { } // Verify that registering tranche tokens is not allowed through extrinsics + #[test_runtimes([altair])] fn register_tranche_asset_blocked() { let mut env = FudgeEnv::::default(); @@ -6094,15 +6071,12 @@ mod altair { ); }); } - - crate::test_for_runtimes!([altair], register_air_works); - crate::test_for_runtimes!([altair], register_foreign_asset_works); - crate::test_for_runtimes!([altair], register_tranche_asset_blocked); } mod currency_id_convert { use super::*; + #[test_runtimes([altair])] fn convert_air() { let mut env = FudgeEnv::::default(); @@ -6139,6 +6113,7 @@ mod altair { /// Verify that Tranche tokens are not handled by the CurrencyIdConvert /// since we don't allow Tranche tokens to be transferable through XCM. + #[test_runtimes([altair])] fn convert_tranche() { let mut env = FudgeEnv::::default(); @@ -6172,6 +6147,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn convert_ausd() { let mut env = FudgeEnv::::default(); @@ -6200,6 +6176,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn convert_ksm() { let mut env = FudgeEnv::::default(); @@ -6220,6 +6197,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn convert_unkown_multilocation() { let mut env = FudgeEnv::::default(); @@ -6233,6 +6211,7 @@ mod altair { }); } + #[test_runtimes([altair])] fn convert_unsupported_currency() { let mut env = FudgeEnv::::default(); @@ -6246,13 +6225,6 @@ mod altair { ) }); } - - crate::test_for_runtimes!([altair], convert_air); - crate::test_for_runtimes!([altair], convert_tranche); - crate::test_for_runtimes!([altair], convert_ausd); - crate::test_for_runtimes!([altair], convert_ksm); - crate::test_for_runtimes!([altair], convert_unkown_multilocation); - crate::test_for_runtimes!([altair], convert_unsupported_currency); } } @@ -6540,6 +6512,7 @@ mod centrifuge { mod asset_registry { use super::*; + #[test_runtimes([centrifuge])] fn register_cfg_works() { let mut env = FudgeEnv::::default(); @@ -6567,6 +6540,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn register_foreign_asset_works() { let mut env = FudgeEnv::::default(); @@ -6598,6 +6572,7 @@ mod centrifuge { } // Verify that registering tranche tokens is not allowed through extrinsics + #[test_runtimes([centrifuge])] fn register_tranche_asset_blocked() { let mut env = FudgeEnv::::default(); @@ -6630,15 +6605,12 @@ mod centrifuge { ); }); } - - crate::test_for_runtimes!([centrifuge], register_cfg_works); - crate::test_for_runtimes!([centrifuge], register_foreign_asset_works); - crate::test_for_runtimes!([centrifuge], register_tranche_asset_blocked); } mod currency_id_convert { use super::*; + #[test_runtimes([centrifuge])] fn convert_cfg() { let mut env = FudgeEnv::::default(); @@ -6677,6 +6649,7 @@ mod centrifuge { /// Verify that even with CFG registered in the AssetRegistry with a XCM /// v2 MultiLocation, that `CurrencyIdConvert` can look it up given an /// identical location in XCM v3. + #[test_runtimes([centrifuge])] fn convert_cfg_xcm_v2() { let mut env = FudgeEnv::::default(); @@ -6715,6 +6688,7 @@ mod centrifuge { /// Verify that a registered token that is NOT XCM transferable is /// filtered out by CurrencyIdConvert as expected. + #[test_runtimes([centrifuge])] fn convert_no_xcm_token() { let mut env = FudgeEnv::::default(); @@ -6728,6 +6702,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn convert_dot() { let mut env = FudgeEnv::::default(); @@ -6748,6 +6723,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn convert_unknown_multilocation() { let mut env = FudgeEnv::::default(); @@ -6764,6 +6740,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn convert_unsupported_currency() { let mut env = FudgeEnv::::default(); @@ -6777,13 +6754,6 @@ mod centrifuge { ) }); } - - crate::test_for_runtimes!([centrifuge], convert_cfg); - crate::test_for_runtimes!([centrifuge], convert_cfg_xcm_v2); - crate::test_for_runtimes!([centrifuge], convert_no_xcm_token); - crate::test_for_runtimes!([centrifuge], convert_dot); - crate::test_for_runtimes!([centrifuge], convert_unknown_multilocation); - crate::test_for_runtimes!([centrifuge], convert_unsupported_currency); } mod restricted_transfers { @@ -6818,6 +6788,7 @@ mod centrifuge { ); } + #[test_runtimes([centrifuge])] fn restrict_cfg_extrinsic() { let mut env = RuntimeEnv::::from_parachain_storage( Genesis::default() @@ -6884,6 +6855,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn restrict_all() { let mut env = RuntimeEnv::::from_parachain_storage( Genesis::default() @@ -7013,6 +6985,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn restrict_lp_eth_usdc_transfer() { let mut env = RuntimeEnv::::from_parachain_storage( Genesis::default() @@ -7103,6 +7076,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn restrict_lp_eth_usdc_lp_transfer() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -7201,6 +7175,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn restrict_usdc_transfer() { let mut env = RuntimeEnv::::from_parachain_storage( Genesis::default() @@ -7272,6 +7247,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn restrict_usdc_xcm_transfer() { let mut env = FudgeEnv::::from_storage( paras::GenesisConfig::> { @@ -7390,6 +7366,7 @@ mod centrifuge { // transfer does not take place. } + #[test_runtimes([centrifuge])] fn restrict_dot_transfer() { let mut env = RuntimeEnv::::from_parachain_storage( Genesis::default() @@ -7477,6 +7454,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn restrict_dot_xcm_transfer() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -7558,15 +7536,6 @@ mod centrifuge { ); }); } - - crate::test_for_runtimes!([centrifuge], restrict_lp_eth_usdc_transfer); - crate::test_for_runtimes!([centrifuge], restrict_lp_eth_usdc_lp_transfer); - crate::test_for_runtimes!([centrifuge], restrict_usdc_transfer); - crate::test_for_runtimes!([centrifuge], restrict_usdc_xcm_transfer); - crate::test_for_runtimes!([centrifuge], restrict_dot_transfer); - crate::test_for_runtimes!([centrifuge], restrict_dot_xcm_transfer); - crate::test_for_runtimes!([centrifuge], restrict_cfg_extrinsic); - crate::test_for_runtimes!([centrifuge], restrict_all); } mod transfers { @@ -7678,6 +7647,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn test_cfg_transfers_to_and_from_sibling() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -7757,6 +7727,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn transfer_ausd_to_centrifuge() { let mut env = FudgeEnv::::default(); @@ -7850,6 +7821,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn transfer_dot_to_and_from_relay_chain() { let mut env = FudgeEnv::::default(); @@ -7900,6 +7872,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn transfer_foreign_sibling_to_centrifuge() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -8008,6 +7981,7 @@ mod centrifuge { }); } + #[test_runtimes([centrifuge])] fn transfer_wormhole_usdc_acala_to_centrifuge() { let mut env = FudgeEnv::::from_storage( Default::default(), @@ -8107,12 +8081,6 @@ mod centrifuge { assert_eq!(bob_balance, 11993571); }); } - - crate::test_for_runtimes!([centrifuge], test_cfg_transfers_to_and_from_sibling); - crate::test_for_runtimes!([centrifuge], transfer_ausd_to_centrifuge); - crate::test_for_runtimes!([centrifuge], transfer_dot_to_and_from_relay_chain); - crate::test_for_runtimes!([centrifuge], transfer_foreign_sibling_to_centrifuge); - crate::test_for_runtimes!([centrifuge], transfer_wormhole_usdc_acala_to_centrifuge); } } @@ -8122,6 +8090,7 @@ mod all { mod restricted_calls { use super::*; + #[test_runtimes(all)] fn xtokens_transfer() { let mut env = FudgeEnv::::default(); @@ -8151,6 +8120,7 @@ mod all { }); } + #[test_runtimes(all)] fn xtokens_transfer_multiasset() { let mut env = FudgeEnv::::default(); @@ -8198,6 +8168,7 @@ mod all { }); } + #[test_runtimes(all)] fn xtokens_transfer_multiassets() { let mut env = FudgeEnv::::default(); @@ -8247,9 +8218,5 @@ mod all { ); }); } - - crate::test_for_runtimes!(all, xtokens_transfer); - crate::test_for_runtimes!(all, xtokens_transfer_multiasset); - crate::test_for_runtimes!(all, xtokens_transfer_multiassets); } } diff --git a/runtime/integration-tests/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index 998aa10adb..26fc9fb16f 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -166,6 +166,7 @@ mod common { max_borrow_amount: ExtMaxBorrowAmount::Quantity(QUANTITY), notional: currency::price_to_currency(PRICE_VALUE_A, Usd6), max_price_variation: rate_from_percent(50), + with_linear_pricing: true, }) } @@ -505,11 +506,17 @@ fn fake_oracle_portfolio_api() { ); // Updating the portfolio with custom prices will use the overriden prices - let collection = [(PRICE_A, common::price_to_usd6(PRICE_VALUE_C))] - .into_iter() - .collect::>() - .try_into() - .unwrap(); + let collection = [( + PRICE_A, + ( + common::price_to_usd6(PRICE_VALUE_C), + pallet_timestamp::Pallet::::now(), + ), + )] + .into_iter() + .collect::>() + .try_into() + .unwrap(); assert_ok!( T::Api::portfolio_valuation(POOL_A, PriceCollectionInput::Custom(collection)), diff --git a/runtime/integration-tests/src/generic/config.rs b/runtime/integration-tests/src/generic/config.rs index d169d143de..ef2f6e568a 100644 --- a/runtime/integration-tests/src/generic/config.rs +++ b/runtime/integration-tests/src/generic/config.rs @@ -91,6 +91,7 @@ pub trait Runtime: Rate = Rate, Quantity = Quantity, PriceId = OracleKey, + Moment = Millis, > + orml_tokens::Config + orml_asset_registry::Config< AssetId = CurrencyId, diff --git a/runtime/integration-tests/src/generic/envs/fudge_env.rs b/runtime/integration-tests/src/generic/envs/fudge_env.rs index ad7dc3dd60..fafa45e860 100644 --- a/runtime/integration-tests/src/generic/envs/fudge_env.rs +++ b/runtime/integration-tests/src/generic/envs/fudge_env.rs @@ -152,6 +152,7 @@ mod tests { use super::*; use crate::generic::{env::Blocks, utils::genesis::Genesis}; + #[test_runtimes(all)] fn correct_nonce_for_submit_later() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() @@ -181,6 +182,4 @@ mod tests { ) .unwrap(); } - - crate::test_for_runtimes!(all, correct_nonce_for_submit_later); } diff --git a/runtime/integration-tests/src/generic/envs/runtime_env.rs b/runtime/integration-tests/src/generic/envs/runtime_env.rs index 8b5720921b..1eb704939a 100644 --- a/runtime/integration-tests/src/generic/envs/runtime_env.rs +++ b/runtime/integration-tests/src/generic/envs/runtime_env.rs @@ -281,6 +281,7 @@ mod tests { use super::*; use crate::generic::{env::Blocks, utils::genesis::Genesis}; + #[test_runtimes(all)] fn correct_nonce_for_submit_now() { let mut env = RuntimeEnv::::from_parachain_storage( Genesis::default() @@ -303,6 +304,7 @@ mod tests { .unwrap(); } + #[test_runtimes(all)] fn correct_nonce_for_submit_later() { let mut env = RuntimeEnv::::from_parachain_storage( Genesis::default() @@ -332,7 +334,4 @@ mod tests { ) .unwrap(); } - - crate::test_for_runtimes!(all, correct_nonce_for_submit_now); - crate::test_for_runtimes!(all, correct_nonce_for_submit_later); } diff --git a/runtime/integration-tests/src/generic/mod.rs b/runtime/integration-tests/src/generic/mod.rs index 60bb9ff3f9..7a484368ed 100644 --- a/runtime/integration-tests/src/generic/mod.rs +++ b/runtime/integration-tests/src/generic/mod.rs @@ -23,33 +23,11 @@ mod cases { } /// Generate tests for the specified runtimes or all runtimes. -/// Usage +/// Usage. Used as building block for #[test_runtimes] procedural macro. /// -/// NOTE: Your probably want to use `#[test_runtimes]` proc macro instead -/// -/// ```rust -/// use crate::generic::config::Runtime; -/// -/// fn foo { -/// /// Your test here... -/// } -/// -/// crate::test_for_runtimes!([development, altair, centrifuge], foo); -/// ``` -/// For the following command: `cargo test -p runtime-integration-tests foo`, -/// it will generate the following output: -/// -/// ```text -/// test generic::foo::altair ... ok -/// test generic::foo::development ... ok -/// test generic::foo::centrifuge ... ok -/// ``` -/// -/// Available input for the first argument is: -/// - Any combination of `development`, `altair`, `centrifuge` inside `[]`. -/// - The world `all`. +/// NOTE: Do not use it direclty, use `#[test_runtimes]` proc macro instead #[macro_export] -macro_rules! test_for_runtimes { +macro_rules! __test_for_runtimes { ( [ $($runtime_name:ident),* ], $test_name:ident ) => { #[cfg(test)] mod $test_name { @@ -73,6 +51,6 @@ macro_rules! test_for_runtimes { } }; ( all , $test_name:ident ) => { - $crate::test_for_runtimes!([development, altair, centrifuge], $test_name); + $crate::__test_for_runtimes!([development, altair, centrifuge], $test_name); }; }