diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/pallet_broker.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/pallet_broker.rs index d5fe933b9bbcc..06ca4f5bc0e7a 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/pallet_broker.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/pallet_broker.rs @@ -652,4 +652,16 @@ impl pallet_broker::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + fn force_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `358` + // Estimated: `3551` + // Minimum execution time: 22_968_000 picoseconds. + Weight::from_parts(23_878_000, 0) + .saturating_add(Weight::from_parts(0, 3551)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/prdoc/pr_10856.prdoc b/prdoc/pr_10856.prdoc new file mode 100644 index 0000000000000..8ab07bf7d04e1 --- /dev/null +++ b/prdoc/pr_10856.prdoc @@ -0,0 +1,11 @@ +title: '[pallet-broker] add extrinsic to force transfer a region' +doc: +- audience: Runtime User + description: |- + Add an extrinsic to `pallet-broker` which allows a privileged origin (`AdminOrigin` or `Root`) to forcefully transfer a region, ignoring its current owner. +crates: +- name: pallet-broker + bump: major +- name: coretime-westend-runtime + bump: minor + \ No newline at end of file diff --git a/substrate/frame/broker/src/benchmarking.rs b/substrate/frame/broker/src/benchmarking.rs index fd1710d738cb0..17233d3ac2dcf 100644 --- a/substrate/frame/broker/src/benchmarking.rs +++ b/substrate/frame/broker/src/benchmarking.rs @@ -1343,6 +1343,41 @@ mod benches { Ok(()) } + #[benchmark] + fn force_transfer() -> Result<(), BenchmarkError> { + 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(sale_data.start_price), + ); + + 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); + + let origin = + T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin, region, recipient.clone()); + + assert_last_event::( + Event::Transferred { + region_id: region, + old_owner: Some(caller), + owner: Some(recipient), + duration: 3u32.into(), + } + .into(), + ); + + Ok(()) + } + // Implements a test for each benchmark. Execute with: // `cargo test -p pallet-broker --features runtime-benchmarks`. impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/substrate/frame/broker/src/lib.rs b/substrate/frame/broker/src/lib.rs index 1317e169e65d4..78d638e36192b 100644 --- a/substrate/frame/broker/src/lib.rs +++ b/substrate/frame/broker/src/lib.rs @@ -1058,6 +1058,25 @@ pub mod pallet { Self::do_remove_potential_renewal(core, when) } + /// Transfer a Bulk Coretime Region to a new owner, ignoring the previous owner. + /// + /// This can also be used to recover regions that have been "burned" (e.g., from an + /// XCM reserve transfer). + /// + /// - `origin`: Must be Root or pass `AdminOrigin`. + /// - `region_id`: The Region whose ownership should change. + /// - `new_owner`: The new owner for the Region. + #[pallet::call_index(28)] + pub fn force_transfer( + origin: OriginFor, + region_id: RegionId, + new_owner: T::AccountId, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin_or_root(origin)?; + Self::do_transfer(region_id, None, new_owner)?; + Ok(()) + } + #[pallet::call_index(99)] #[pallet::weight(T::WeightInfo::swap_leases())] pub fn swap_leases(origin: OriginFor, id: TaskId, other: TaskId) -> DispatchResult { diff --git a/substrate/frame/broker/src/tests.rs b/substrate/frame/broker/src/tests.rs index a65074c7e3d35..7c8abcdb5d8b6 100644 --- a/substrate/frame/broker/src/tests.rs +++ b/substrate/frame/broker/src/tests.rs @@ -2901,3 +2901,124 @@ fn remove_potential_renewal_makes_auto_renewal_die() { assert_eq!(AutoRenewals::::get().len(), 0); }) } + +#[test] +fn force_transfer_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 4)); + advance_to(2); + + const OLD_OWNER: u64 = 1; + const NEW_OWNER: u64 = 222; + + let region_id = Broker::do_purchase(OLD_OWNER, u64::max_value()).unwrap(); + let region = Regions::::get(region_id).unwrap(); + + assert_noop!( + Broker::force_transfer( + RuntimeOrigin::root(), + RegionId { + begin: u32::max_value(), + core: u16::max_value(), + mask: CoreMask::void() + }, + NEW_OWNER + ), + Error::::UnknownRegion + ); + + assert_noop!( + Broker::force_transfer(RuntimeOrigin::signed(1001), region_id, NEW_OWNER), + BadOrigin + ); + + assert_ok!(Broker::force_transfer(RuntimeOrigin::root(), region_id, NEW_OWNER)); + + System::assert_last_event( + Event::Transferred { + region_id, + duration: region.end - region_id.begin, + old_owner: Some(OLD_OWNER), + owner: Some(NEW_OWNER), + } + .into(), + ); + + assert_noop!( + Broker::assign(RuntimeOrigin::signed(OLD_OWNER), region_id, 10, Finality::Final), + Error::::NotOwner + ); + + assert_ok!(Broker::assign( + RuntimeOrigin::signed(NEW_OWNER), + region_id, + 10, + Finality::Final + )); + }); +} + +#[test] +fn force_transfer_can_transfer_burned_region() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 4)); + advance_to(2); + + const OLD_OWNER: u64 = 1; + const NEW_OWNER: u64 = 222; + + let region_id = Broker::do_purchase(OLD_OWNER, u64::max_value()).unwrap(); + + assert_ok!(>::burn(®ion_id.into(), None)); + + let region = Regions::::get(region_id).unwrap(); + assert_eq!(region.owner, None); + + assert_ok!(Broker::force_transfer(RuntimeOrigin::root(), region_id, NEW_OWNER)); + + System::assert_last_event( + Event::Transferred { + region_id, + duration: region.end - region_id.begin, + old_owner: None, + owner: Some(NEW_OWNER), + } + .into(), + ); + + assert_ok!(Broker::assign( + RuntimeOrigin::signed(NEW_OWNER), + region_id, + 10, + Finality::Final + )); + }); +} + +#[test] +fn force_transfer_can_transfer_provisionally_assigned_region() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 4)); + advance_to(2); + + const OLD_OWNER: u64 = 1; + const NEW_OWNER: u64 = 222; + + let region_id = Broker::do_purchase(OLD_OWNER, u64::max_value()).unwrap(); + + assert_ok!(Broker::assign(RuntimeOrigin::signed(OLD_OWNER), region_id, 1001, Provisional)); + + assert_ok!(Broker::force_transfer(RuntimeOrigin::root(), region_id, NEW_OWNER)); + + let region = Regions::::get(region_id).unwrap(); + System::assert_last_event( + Event::Transferred { + region_id, + duration: region.end - region_id.begin, + old_owner: Some(OLD_OWNER), + owner: Some(NEW_OWNER), + } + .into(), + ); + }); +} diff --git a/substrate/frame/broker/src/weights.rs b/substrate/frame/broker/src/weights.rs index dffacd176d7b5..6b183de804fee 100644 --- a/substrate/frame/broker/src/weights.rs +++ b/substrate/frame/broker/src/weights.rs @@ -108,6 +108,7 @@ pub trait WeightInfo { fn on_new_timeslice() -> Weight; fn remove_assignment() -> Weight; fn remove_potential_renewal() -> Weight; + fn force_transfer() -> Weight; } /// Weights for `pallet_broker` using the Substrate node and recommended hardware. @@ -618,6 +619,17 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + fn force_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `496` + // Estimated: `3551` + // Minimum execution time: 22_866_000 picoseconds. + Weight::from_parts(23_961_000, 3551) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } // For backwards compatibility and tests. @@ -1127,4 +1139,15 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + fn force_transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `496` + // Estimated: `3551` + // Minimum execution time: 22_866_000 picoseconds. + Weight::from_parts(23_961_000, 3551) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } }