diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/coretime.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/coretime.rs index 54cfc64930b80..0c9570de4e2a1 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/coretime.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/coretime.rs @@ -113,6 +113,7 @@ parameter_types! { pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); pub const MinimumCreditPurchase: Balance = UNITS / 10; pub RevenueAccumulationAccount: AccountId = BrokerPalletId::get().into_sub_account_truncating(b"burnstash"); + pub const MinimumEndPrice: Balance = UNITS; } /// Type that implements the `CoretimeInterface` for the allocation of Coretime. Meant to operate @@ -319,6 +320,6 @@ impl pallet_broker::Config for Runtime { type AdminOrigin = EnsureRoot; type SovereignAccountOf = SovereignAccountOf; type MaxAutoRenewals = ConstU32<100>; - type PriceAdapter = pallet_broker::CenterTargetPrice; + type PriceAdapter = pallet_broker::MinimumPrice; type MinimumCreditPurchase = MinimumCreditPurchase; } diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs index 9aa9e699b65da..1f2385b333f45 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs @@ -113,6 +113,7 @@ parameter_types! { pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); pub const MinimumCreditPurchase: Balance = UNITS / 10; pub RevenueAccumulationAccount: AccountId = BrokerPalletId::get().into_sub_account_truncating(b"burnstash"); + pub const MinimumEndPrice: Balance = UNITS; } /// Type that implements the `CoretimeInterface` for the allocation of Coretime. Meant to operate @@ -332,6 +333,6 @@ impl pallet_broker::Config for Runtime { type AdminOrigin = EnsureRoot; type SovereignAccountOf = SovereignAccountOf; type MaxAutoRenewals = ConstU32<20>; - type PriceAdapter = pallet_broker::CenterTargetPrice; + type PriceAdapter = pallet_broker::MinimumPrice; type MinimumCreditPurchase = MinimumCreditPurchase; } diff --git a/prdoc/pr_8630.prdoc b/prdoc/pr_8630.prdoc new file mode 100644 index 0000000000000..d079ee89c79a5 --- /dev/null +++ b/prdoc/pr_8630.prdoc @@ -0,0 +1,24 @@ +title: "Broker: Introduce min price and adjust renewals to lower market" + +doc: +- audience: Runtime Dev + description: |- + pallet-broker now provides an additional `AdaptPrice` implementation: + `MinimumPrice`. This price adapter works exactly the same as the + `CenterTargetPrice` adapter, except that it can be configured with a + minimum price. If set, it will never drop the returned `end_price` (nor the + `target_price`) below that minimum. + + Apart from having an adapter to ensure a minimum price, the behavior of + renewals was also adjusted: Renewals are now either bumped by renewal bump + or set to the `end_price` of the current sale - whatever number is higher. + This ensures some market coupling of renewal prices, while still + maintaining some predictability. + +crates: +- name: pallet-broker + bump: minor +- name: coretime-rococo-runtime + bump: minor +- name: coretime-westend-runtime + bump: minor diff --git a/substrate/frame/broker/src/adapt_price.rs b/substrate/frame/broker/src/adapt_price.rs index 9b2e1dd8997bd..47823b3c09193 100644 --- a/substrate/frame/broker/src/adapt_price.rs +++ b/substrate/frame/broker/src/adapt_price.rs @@ -19,6 +19,7 @@ use crate::{CoreIndex, SaleInfoRecord}; use sp_arithmetic::{traits::One, FixedU64}; +use sp_core::{Get, RuntimeDebug}; use sp_runtime::{FixedPointNumber, FixedPointOperand, Saturating}; /// Performance of a past sale. @@ -43,7 +44,7 @@ pub struct SalePerformance { } /// Result of `AdaptPrice::adapt_price`. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)] pub struct AdaptedPrices { /// New minimum price to use. pub end_price: Balance, @@ -135,8 +136,39 @@ impl AdaptPrice for CenterTargetPrice(core::marker::PhantomData<(Balance, MinPrice)>); + +impl> AdaptPrice + for MinimumPrice +{ + fn leadin_factor_at(when: FixedU64) -> FixedU64 { + CenterTargetPrice::::leadin_factor_at(when) + } + + fn adapt_price(performance: SalePerformance) -> AdaptedPrices { + let mut proposal = CenterTargetPrice::::adapt_price(performance); + let min_price = MinPrice::get(); + if proposal.end_price < min_price { + proposal.end_price = min_price; + } + // Fix target price if necessary: + if proposal.target_price < proposal.end_price { + proposal.target_price = proposal.end_price; + } + proposal + } +} + #[cfg(test)] mod tests { + use sp_core::ConstU64; + use super::*; #[test] @@ -240,4 +272,38 @@ mod tests { let prices = CenterTargetPrice::adapt_price(performance); assert_eq!(prices.target_price, 1000); } + + #[test] + fn minimum_price_works() { + let performance = SalePerformance::new(Some(10), 10); + let prices = MinimumPrice::>::adapt_price(performance); + assert_eq!(prices.end_price, 10); + assert_eq!(prices.target_price, 10); + } + + #[test] + fn minimum_price_does_not_affect_valid_target_price() { + let performance = SalePerformance::new(Some(12), 10); + let prices = MinimumPrice::>::adapt_price(performance); + assert_eq!(prices.end_price, 10); + assert_eq!(prices.target_price, 12); + } + + #[test] + fn no_minimum_price_works_as_center_target_price() { + let performances = [ + (Some(100), 10), + (None, 20), + (Some(1000), 10), + (Some(10), 10), + (Some(1), 1), + (Some(0), 10), + ]; + for (sellout, end) in performances { + let performance = SalePerformance::new(sellout, end); + let prices_minimum = MinimumPrice::>::adapt_price(performance); + let prices = CenterTargetPrice::adapt_price(performance); + assert_eq!(prices, prices_minimum); + } + } } diff --git a/substrate/frame/broker/src/benchmarking.rs b/substrate/frame/broker/src/benchmarking.rs index 58c8689c54966..517e2892489e1 100644 --- a/substrate/frame/broker/src/benchmarking.rs +++ b/substrate/frame/broker/src/benchmarking.rs @@ -30,11 +30,11 @@ use frame_support::{ }, }; use frame_system::{Pallet as System, RawOrigin}; -use sp_arithmetic::Perbill; +use sp_arithmetic::{FixedU64, Perbill}; use sp_core::Get; use sp_runtime::{ traits::{BlockNumberProvider, MaybeConvert}, - Saturating, + FixedPointNumber, Saturating, }; const SEED: u32 = 0; @@ -97,7 +97,13 @@ fn advance_to(b: u32) { } } -fn setup_and_start_sale() -> Result { +struct StartedSale { + start_price: Balance, + end_price: Balance, + first_core: CoreIndex, +} + +fn setup_and_start_sale() -> Result>, BenchmarkError> { Configuration::::put(new_config_record::()); // Assume Reservations to be filled for worst case @@ -106,13 +112,35 @@ fn setup_and_start_sale() -> Result { // Assume Leases to be filled for worst case setup_leases::(T::MaxLeasedCores::get(), 1, 10); - Broker::::do_start_sales(10_000_000u32.into(), MAX_CORE_COUNT.into()) + let initial_price = 10_000_000u32.into(); + let (start_price, end_price) = get_start_end_price::(initial_price); + Broker::::do_start_sales(initial_price, MAX_CORE_COUNT.into()) .map_err(|_| BenchmarkError::Weightless)?; - Ok(T::MaxReservedCores::get() - .saturating_add(T::MaxLeasedCores::get()) - .try_into() - .unwrap()) + let sale_data = StartedSale { + start_price, + end_price, + first_core: T::MaxReservedCores::get() + .saturating_add(T::MaxLeasedCores::get()) + .try_into() + .unwrap(), + }; + + Ok(sale_data) +} + +fn get_start_end_price(initial_price: BalanceOf) -> (BalanceOf, BalanceOf) { + let end_price = ::PriceAdapter::adapt_price(SalePerformance { + sellout_price: None, + end_price: initial_price, + ideal_cores_sold: 0, + cores_offered: 0, + cores_sold: 0, + }) + .end_price; + let start_price = ::PriceAdapter::leadin_factor_at(FixedU64::from(0)) + .saturating_mul_int(end_price); + (start_price, end_price) } #[benchmarks] @@ -234,7 +262,7 @@ mod benches { let latest_region_begin = Broker::::latest_timeslice_ready_to_commit(&config); let initial_price = 10_000_000u32.into(); - + let (start_price, end_price) = get_start_end_price::(initial_price); let origin = T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; @@ -248,8 +276,8 @@ mod benches { Event::SaleInitialized { sale_start, leadin_length: 1u32.into(), - start_price: 1_000_000_000u32.into(), - end_price: 10_000_000u32.into(), + start_price, + end_price, region_begin: latest_region_begin + config.region_length, region_end: latest_region_begin + config.region_length * 2, ideal_cores_sold: 0, @@ -267,29 +295,29 @@ mod benches { #[benchmark] fn purchase() -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); #[extrinsic_call] - _(RawOrigin::Signed(caller.clone()), 10_000_000u32.into()); + _(RawOrigin::Signed(caller.clone()), sale_data.start_price); - assert_eq!(SaleInfo::::get().unwrap().sellout_price, Some(10_000_000u32.into())); + assert_eq!(SaleInfo::::get().unwrap().sellout_price.unwrap(), sale_data.end_price); assert_last_event::( Event::Purchased { who: caller, region_id: RegionId { begin: SaleInfo::::get().unwrap().region_begin, - core, + core: sale_data.first_core, mask: CoreMask::complete(), }, - price: 10_000_000u32.into(), + price: sale_data.end_price, duration: 3u32.into(), } .into(), @@ -300,7 +328,7 @@ mod benches { #[benchmark] fn renew() -> Result<(), BenchmarkError> { - setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; let region_len = Configuration::::get().unwrap().region_length; advance_to::(2); @@ -308,10 +336,10 @@ mod benches { let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(20_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); Broker::::do_assign(region, None, 1001, Final) @@ -330,17 +358,17 @@ mod benches { #[benchmark] fn transfer() -> Result<(), BenchmarkError> { - setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); let recipient: T::AccountId = account("recipient", 0, SEED); @@ -363,17 +391,17 @@ mod benches { #[benchmark] fn partition() -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); #[extrinsic_call] @@ -381,10 +409,22 @@ mod benches { assert_last_event::( Event::Partitioned { - old_region_id: RegionId { begin: region.begin, core, mask: CoreMask::complete() }, + old_region_id: RegionId { + begin: region.begin, + core: sale_data.first_core, + mask: CoreMask::complete(), + }, new_region_ids: ( - RegionId { begin: region.begin, core, mask: CoreMask::complete() }, - RegionId { begin: region.begin + 2, core, mask: CoreMask::complete() }, + RegionId { + begin: region.begin, + core: sale_data.first_core, + mask: CoreMask::complete(), + }, + RegionId { + begin: region.begin + 2, + core: sale_data.first_core, + mask: CoreMask::complete(), + }, ), } .into(), @@ -395,17 +435,18 @@ mod benches { #[benchmark] fn interlace() -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; + let core = sale_data.first_core; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); #[extrinsic_call] @@ -431,17 +472,18 @@ mod benches { #[benchmark] fn assign() -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; + let core = sale_data.first_core; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); #[extrinsic_call] @@ -466,17 +508,18 @@ mod benches { #[benchmark] fn pool() -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; + let core = sale_data.first_core; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); let recipient: T::AccountId = account("recipient", 0, SEED); @@ -502,21 +545,22 @@ mod benches { fn claim_revenue( m: Linear<1, { new_config_record::().region_length }>, ) -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; + let core = sale_data.first_core; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); T::Currency::set_balance( &Broker::::account_id(), T::Currency::minimum_balance().saturating_add(200_000_000u32.into()), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); let recipient: T::AccountId = account("recipient", 0, SEED); @@ -591,7 +635,8 @@ mod benches { #[benchmark] fn drop_region() -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; + let core = sale_data.first_core; let region_len = Configuration::::get().unwrap().region_length; advance_to::(2); @@ -599,10 +644,10 @@ mod benches { let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); advance_to::( @@ -625,7 +670,8 @@ mod benches { #[benchmark] fn drop_contribution() -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; + let core = sale_data.first_core; let region_len = Configuration::::get().unwrap().region_length; advance_to::(2); @@ -633,10 +679,10 @@ mod benches { let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); let recipient: T::AccountId = account("recipient", 0, SEED); @@ -693,7 +739,8 @@ mod benches { #[benchmark] fn drop_renewal() -> Result<(), BenchmarkError> { - let core = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; + let core = sale_data.first_core; let when = 5u32.into(); let region_len = Configuration::::get().unwrap().region_length; @@ -822,8 +869,10 @@ mod benches { setup_leases::(n_leases, 1, 20); // Start sales so we can test the auto-renewals. + let initial_price = 10_000_000u32.into(); + let (start_price, _) = get_start_end_price::(initial_price); Broker::::do_start_sales( - 10_000_000u32.into(), + initial_price, n.saturating_sub(n_reservations) .saturating_sub(n_leases) .try_into() @@ -842,16 +891,20 @@ mod benches { let timeslice_period: u32 = T::TimeslicePeriod::get().try_into().ok().unwrap(); let sale = SaleInfo::::get().expect("Sale has started."); + let now = RCBlockNumberProviderOf::::current_block_number(); + let price = Broker::::sale_price(&sale, now); (0..n_renewable.into()).try_for_each(|indx| -> Result<(), BenchmarkError> { let task = 1000 + indx; let caller: T::AccountId = T::SovereignAccountOf::maybe_convert(task) .expect("Failed to get sovereign account"); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(100_000_000u32.into()), + T::Currency::minimum_balance() + .saturating_add(start_price) + .saturating_add(start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), start_price) .expect("Offer not high enough for configuration."); Broker::::do_assign(region, None, task, Final) @@ -914,7 +967,7 @@ mod benches { who, old_core: n_reservations as u16 + n_leases as u16 + indx as u16, core: n_reservations as u16 + n_leases as u16 + indx as u16, - price: 10_000_000u32.into(), + price, begin: new_sale.region_begin, duration: config.region_length, workload: Schedule::truncate_from(vec![ScheduleItem { @@ -1097,7 +1150,7 @@ mod benches { #[benchmark] fn enable_auto_renew() -> Result<(), BenchmarkError> { - let _core_id = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; advance_to::(2); @@ -1110,10 +1163,10 @@ mod benches { // Sovereign account needs sufficient funds to purchase and renew. T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(100_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); Broker::::do_assign(region, None, task, Final) @@ -1129,11 +1182,12 @@ mod benches { // Sovereign account needs sufficient funds to purchase and renew. T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(100_000_000u32.into()), + T::Currency::minimum_balance() + .saturating_add(sale_data.start_price.saturating_add(sale_data.start_price)), ); // The region for which we benchmark enable auto renew. - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); Broker::::do_assign(region, None, 2001, Final) .map_err(|_| BenchmarkError::Weightless)?; @@ -1161,7 +1215,8 @@ mod benches { #[benchmark] fn disable_auto_renew() -> Result<(), BenchmarkError> { - let core_id = setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; + let core = sale_data.first_core; advance_to::(2); @@ -1173,10 +1228,10 @@ mod benches { .expect("Failed to get sovereign account"); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); Broker::::do_assign(region, None, task, Final) @@ -1193,26 +1248,26 @@ mod benches { T::SovereignAccountOf::maybe_convert(task).expect("Failed to get sovereign account"); #[extrinsic_call] - _(RawOrigin::Signed(caller), core_id, task); + _(RawOrigin::Signed(caller), core, task); - assert_last_event::(Event::AutoRenewalDisabled { core: core_id, task }.into()); + assert_last_event::(Event::AutoRenewalDisabled { core, task }.into()); Ok(()) } #[benchmark] fn on_new_timeslice() -> Result<(), BenchmarkError> { - setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let _region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let _region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); let timeslice = Broker::::current_timeslice(); @@ -1227,17 +1282,17 @@ mod benches { #[benchmark] fn remove_assignment() -> Result<(), BenchmarkError> { - setup_and_start_sale::()?; + let sale_data = setup_and_start_sale::()?; advance_to::(2); let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(10_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(sale_data.start_price), ); - let region = Broker::::do_purchase(caller.clone(), 10_000_000u32.into()) + let region = Broker::::do_purchase(caller.clone(), sale_data.start_price) .expect("Offer not high enough for configuration."); Broker::::do_assign(region, None, 1000, Provisional) diff --git a/substrate/frame/broker/src/dispatchable_impls.rs b/substrate/frame/broker/src/dispatchable_impls.rs index 79d671d7432c4..41ff7967bcb82 100644 --- a/substrate/frame/broker/src/dispatchable_impls.rs +++ b/substrate/frame/broker/src/dispatchable_impls.rs @@ -15,6 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use core::cmp; + use super::*; use frame_support::{ pallet_prelude::*, @@ -200,7 +202,9 @@ impl Pallet { Workplan::::insert((sale.region_begin, core), &workload); let begin = sale.region_end; - let price_cap = record.price + config.renewal_bump * record.price; + let end_price = sale.end_price; + // Renewals should never be priced lower than the current `end_price`: + let price_cap = cmp::max(record.price + config.renewal_bump * record.price, end_price); let now = RCBlockNumberProviderOf::::current_block_number(); let price = Self::sale_price(&sale, now).min(price_cap); log::debug!( diff --git a/substrate/frame/broker/src/tests.rs b/substrate/frame/broker/src/tests.rs index 2e04f7201f87b..4f489527eba3d 100644 --- a/substrate/frame/broker/src/tests.rs +++ b/substrate/frame/broker/src/tests.rs @@ -495,6 +495,75 @@ fn renewals_affect_price() { }); } +#[test] +/// Renewals adjust to lower end of market +fn renewal_price_adjusts_to_lower_market_end() { + sp_tracing::try_init_simple(); + let b = 100_000_000; + let region_length_blocks = 40; + let config = ConfigRecord { + advance_notice: 2, + interlude_length: 10, + leadin_length: 20, + ideal_bulk_proportion: Perbill::from_percent(100), + limit_cores_offered: None, + // Region length is in time slices (2 blocks): + region_length: 20, + renewal_bump: Perbill::from_percent(10), + contribution_timeout: 5, + }; + TestExt::new_with_config(config.clone()) + .endow(1, b) + .endow(2, b) + .execute_with(|| { + let price = 910; + assert_ok!(Broker::do_start_sales(10, 2)); + advance_to(11); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + // Price is lower, because already one block in: + let b = b - price; + assert_eq!(balance(1), b); + assert_ok!(Broker::do_assign(region, None, 1001, Final)); + advance_to(region_length_blocks); + assert_noop!(Broker::do_purchase(1, u64::max_value()), Error::::TooEarly); + + let core = Broker::do_renew(1, region.core).unwrap(); + // First renewal has same price as initial purchase. + let b = b - price; + assert_eq!(balance(1), b); + // Ramp up price: + advance_to(region_length_blocks + config.interlude_length + 1); + Broker::do_purchase(2, u64::max_value()).unwrap(); + + advance_to(2 * region_length_blocks); + assert_ok!(Broker::do_renew(1, core)); + // Renewal bump in effect + let price = price + Perbill::from_percent(10) * price; + let b = b - price; + assert_eq!(balance(1), b); + // Ramp up price again: + advance_to(2 * region_length_blocks + config.interlude_length + 1); + Broker::do_purchase(2, u64::max_value()).unwrap(); + + advance_to(3 * region_length_blocks); + assert_ok!(Broker::do_renew(1, core)); + // Renewal bump still in effect + let price = price + Perbill::from_percent(10) * price; + let b = b - price; + assert_eq!(balance(1), b); + // No further price ramp up necessary - the price of this sale is relevant for next + // renewal. + let end_price = SaleInfo::::get().unwrap().end_price; + + advance_to(4 * region_length_blocks); + assert_ok!(Broker::do_renew(1, core)); + // Renewal bump trumped by end price of previous sale. + let price = end_price; + let b = b - price; + assert_eq!(balance(1), b); + }); +} + #[test] fn instapool_payouts_work() { // Commented out code is from the reference test implementation and should be uncommented as