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 da94865ca3..e98cf1c71d 100644 --- a/pallets/loans/src/entities/loans.rs +++ b/pallets/loans/src/entities/loans.rs @@ -31,6 +31,7 @@ use crate::{ BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanRestrictions, MutationError, RepaidAmount, RepayLoanError, RepayRestrictions, RepaymentSchedule, }, + PriceOf, }; /// Loan information. @@ -292,7 +293,7 @@ impl ActiveLoan { pub fn present_value_by( &self, rates: &Rates, - prices: &BTreeMap, + prices: &BTreeMap>, ) -> Result where Rates: RateCollection, @@ -583,3 +584,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::{LoanRestrictions, RepaidAmount, RepaymentSchedule}, + 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 ea6d691c43..dbe4ddb994 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,12 +154,27 @@ 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: Seconds, + price: T::Balance, + price_last_updated: Seconds, + ) -> Result { + if 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); + } + + Ok(cfg_utils::math::y_coord_in_rect( + (min(price_last_updated, maturity), price), + (maturity, self.info.notional), + min(T::Time::now(), maturity), + )?) + } else { + Ok(price) + } } pub fn current_price( @@ -163,12 +182,31 @@ impl ExternalActivePricing { pool_id: T::PoolId, maturity: Option, ) -> Result { - let maturity = maturity.unwrap_or(T::Time::now()); + self.current_price_inner( + maturity, + T::PriceRegistry::get(&self.info.price_id, &pool_id).ok(), + ) + } - Ok(match T::PriceRegistry::get(&self.info.price_id, &pool_id) { - Ok(data) => data.0, - Err(_) => self.linear_accrual_price(maturity)?, - }) + fn current_price_inner( + &self, + maturity: Option, + oracle: Option>, + ) -> Result { + let maturity = maturity.unwrap_or(T::Time::now()); + 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( @@ -199,16 +237,10 @@ impl ExternalActivePricing { pub fn present_value_cached( &self, - cache: &BTreeMap, + cache: &BTreeMap>, maturity: Option, ) -> Result { - let price = match cache.get(&self.info.price_id) { - Some(data) => *data, - None => { - let maturity = maturity.unwrap_or(T::Time::now()); - 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)?) } @@ -293,3 +325,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/lib.rs b/pallets/loans/src/lib.rs index c1648aca70..8b592b2eab 100644 --- a/pallets/loans/src/lib.rs +++ b/pallets/loans/src/lib.rs @@ -118,7 +118,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)] @@ -169,7 +169,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 @@ -232,7 +232,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, @@ -259,7 +259,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, @@ -1054,7 +1054,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() @@ -1062,7 +1062,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/mock.rs b/pallets/loans/src/tests/mock.rs index c000cf79f6..6bcdd32e2a 100644 --- a/pallets/loans/src/tests/mock.rs +++ b/pallets/loans/src/tests/mock.rs @@ -71,7 +71,7 @@ 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); diff --git a/pallets/loans/src/tests/mod.rs b/pallets/loans/src/tests/mod.rs index 05349a47dc..13605dde37 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 2d0f4c68d4..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)); }); } @@ -219,6 +221,56 @@ 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(|| { 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 5cb6b7bac2..b00087071e 100644 --- a/pallets/loans/src/tests/util.rs +++ b/pallets/loans/src/tests/util.rs @@ -148,6 +148,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/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 0a3e278011..f10ab5d54a 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -1419,6 +1419,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 5b81a46983..44c518f855 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -1427,6 +1427,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 72d939c146..7649078801 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -1036,6 +1036,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/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index dc9aaac873..00c3cdfe63 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -165,6 +165,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, }) } @@ -504,11 +505,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 ffc587c207..30ec4ff1b0 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,