diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 24d3845f7bb30..c564e9457f56b 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -88,7 +88,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = Balances; + type NativeBalance = Balances; type SubmitOrigin = frame_system::EnsureSigned; type CancelOrigin = EitherOf, ReferendumCanceller>; type KillOrigin = EitherOf, ReferendumKiller>; diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/ambassador/mod.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/ambassador/mod.rs index 9711a34589de8..6bfa95aaaa162 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/ambassador/mod.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/ambassador/mod.rs @@ -135,7 +135,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = Balances; + type NativeBalance = Balances; // A proposal can be submitted by a member of the Ambassador Program of // [ranks::SENIOR_AMBASSADOR_TIER_3] rank or higher. type SubmitOrigin = pallet_ranked_collective::EnsureMember< diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs index ceadc131c384e..5c9f9eaca8041 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs @@ -80,7 +80,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = Balances; + type NativeBalance = Balances; // Fellows can submit proposals. type SubmitOrigin = EitherOf< pallet_ranked_collective::EnsureMember, diff --git a/polkadot/runtime/rococo/src/governance/fellowship.rs b/polkadot/runtime/rococo/src/governance/fellowship.rs index cb194e05e9ce9..d6a68a5dfeba7 100644 --- a/polkadot/runtime/rococo/src/governance/fellowship.rs +++ b/polkadot/runtime/rococo/src/governance/fellowship.rs @@ -299,7 +299,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = Balances; + type NativeBalance = Balances; type SubmitOrigin = pallet_ranked_collective::EnsureMember; type CancelOrigin = FellowshipExperts; diff --git a/polkadot/runtime/rococo/src/governance/mod.rs b/polkadot/runtime/rococo/src/governance/mod.rs index 4cd773960da59..440a0889e1ec7 100644 --- a/polkadot/runtime/rococo/src/governance/mod.rs +++ b/polkadot/runtime/rococo/src/governance/mod.rs @@ -79,7 +79,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = Balances; + type NativeBalance = Balances; type SubmitOrigin = frame_system::EnsureSigned; type CancelOrigin = EitherOf, ReferendumCanceller>; type KillOrigin = EitherOf, ReferendumKiller>; diff --git a/polkadot/runtime/westend/src/governance/mod.rs b/polkadot/runtime/westend/src/governance/mod.rs index 50d29bca6e874..6fe5fd5758f2e 100644 --- a/polkadot/runtime/westend/src/governance/mod.rs +++ b/polkadot/runtime/westend/src/governance/mod.rs @@ -83,7 +83,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = Balances; + type NativeBalance = Balances; type SubmitOrigin = frame_system::EnsureSigned; type CancelOrigin = EitherOf, ReferendumCanceller>; type KillOrigin = EitherOf, ReferendumKiller>; diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 72e8d1e1f6c0f..d460e15ccd7e5 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -1051,7 +1051,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = pallet_balances::Pallet; + type NativeBalance = pallet_balances::Pallet; type SubmitOrigin = EnsureSigned; type CancelOrigin = EnsureRoot; type KillOrigin = EnsureRoot; diff --git a/substrate/frame/referenda/src/benchmarking.rs b/substrate/frame/referenda/src/benchmarking.rs index 59499d9c8bf08..ceab51aeb8841 100644 --- a/substrate/frame/referenda/src/benchmarking.rs +++ b/substrate/frame/referenda/src/benchmarking.rs @@ -26,7 +26,10 @@ use frame_benchmarking::v1::{ }; use frame_support::{ assert_ok, - traits::{Currency, EnsureOrigin, EnsureOriginWithArg, UnfilteredDispatchable}, + traits::{ + tokens::{Fortitude, Preservation}, + EnsureOrigin, EnsureOriginWithArg, UnfilteredDispatchable, + }, }; use frame_system::RawOrigin; use sp_runtime::traits::Bounded as ArithBounded; @@ -41,9 +44,16 @@ fn assert_last_event, I: 'static>(generic_event: >:: frame_system::Pallet::::assert_last_event(generic_event.into()); } -fn funded_account, I: 'static>(name: &'static str, index: u32) -> T::AccountId { +fn funded_account, I: 'static>( + name: &'static str, + index: u32, + decision_deposit: BalanceOf, +) -> T::AccountId { let caller: T::AccountId = account(name, index, SEED); - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + //Giving enough balance to cover the decision deposit + the submission deposit + buffer for + // fees + let amount = T::NativeBalance::minimum_balance() + decision_deposit.saturating_mul(2u32.into()); + T::NativeBalance::set_balance(&caller, amount); caller } @@ -55,7 +65,7 @@ fn dummy_call, I: 'static>() -> BoundedCallOf { fn create_referendum, I: 'static>(origin: T::RuntimeOrigin) -> ReferendumIndex { if let Ok(caller) = frame_system::ensure_signed(origin.clone()) { - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + T::NativeBalance::set_balance(&caller, BalanceOf::::max_value() / 2u32.into()); whitelist_account!(caller); } @@ -69,8 +79,11 @@ fn create_referendum, I: 'static>(origin: T::RuntimeOrigin) -> Refe } fn place_deposit, I: 'static>(index: ReferendumIndex) { - let caller = funded_account::("caller", 0); + let track_info = info::(index); + let decision_deposit = track_info.decision_deposit; + let caller = funded_account::("caller", 0, decision_deposit); whitelist_account!(caller); + assert_ok!(Referenda::::place_decision_deposit(RawOrigin::Signed(caller).into(), index)); } @@ -202,7 +215,7 @@ benchmarks_instance_pallet! { let origin = T::SubmitOrigin::try_successful_origin(&RawOrigin::Root.into()).map_err(|_| BenchmarkError::Weightless)?; if let Ok(caller) = frame_system::ensure_signed(origin.clone()) { - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + T::NativeBalance::set_balance(&caller, BalanceOf::::max_value()/2u32.into()); whitelist_account!(caller); } }: _( @@ -291,7 +304,7 @@ benchmarks_instance_pallet! { T::SubmitOrigin::try_successful_origin(&RawOrigin::Root.into()).map_err(|_| BenchmarkError::Weightless)?; let index = create_referendum::(origin.clone()); let caller = frame_system::ensure_signed(origin.clone()).unwrap(); - let balance = T::Currency::free_balance(&caller); + let balance = T::NativeBalance::reducible_balance(&caller, Preservation::Preserve, Fortitude::Polite); assert_ok!(Referenda::::cancel( T::CancelOrigin::try_successful_origin() .expect("CancelOrigin has no successful origin required for the benchmark"), @@ -301,7 +314,7 @@ benchmarks_instance_pallet! { }: _(origin, index) verify { assert_matches!(ReferendumInfoFor::::get(index), Some(ReferendumInfo::Cancelled(_, None, _))); - let new_balance = T::Currency::free_balance(&caller); + let new_balance = T::NativeBalance::reducible_balance(&caller, Preservation::Preserve, Fortitude::Polite); // the deposit is zero or make sure it was unreserved. assert!(T::SubmissionDeposit::get().is_zero() || new_balance > balance); } diff --git a/substrate/frame/referenda/src/lib.rs b/substrate/frame/referenda/src/lib.rs index 11c1be06382c4..0cb43ca7ecdbe 100644 --- a/substrate/frame/referenda/src/lib.rs +++ b/substrate/frame/referenda/src/lib.rs @@ -73,12 +73,14 @@ use frame_support::{ dispatch::DispatchResult, ensure, traits::{ + fungible::{Balanced, Inspect, InspectHold, Mutate, MutateHold}, schedule::{ v3::{Anon as ScheduleAnon, Named as ScheduleNamed}, DispatchTime, }, - Currency, LockIdentifier, OnUnbalanced, OriginTrait, PollStatus, Polling, QueryPreimage, - ReservableCurrency, StorePreimage, VoteTally, + tokens::{Fortitude, Precision}, + LockIdentifier, OnUnbalanced, OriginTrait, PollStatus, Polling, QueryPreimage, + StorePreimage, VoteTally, }, BoundedVec, }; @@ -97,11 +99,10 @@ use self::branch::{BeginDecidingBranch, OneFewerDecidingBranch, ServiceBranch}; pub use self::{ pallet::*, types::{ - BalanceOf, BlockNumberFor, BoundedCallOf, CallOf, ConstTrackInfo, Curve, DecidingStatus, - DecidingStatusOf, Deposit, InsertSorted, NegativeImbalanceOf, PalletsOriginOf, - ReferendumIndex, ReferendumInfo, ReferendumInfoOf, ReferendumStatus, ReferendumStatusOf, - ScheduleAddressOf, StringLike, TallyOf, Track, TrackIdOf, TrackInfo, TrackInfoOf, - TracksInfo, VotesOf, + BalanceOf, BlockNumberFor, BoundedCallOf, CallOf, ConstTrackInfo, Curve, DebtOf, + DecidingStatus, DecidingStatusOf, Deposit, InsertSorted, PalletsOriginOf, ReferendumIndex, + ReferendumInfo, ReferendumInfoOf, ReferendumStatus, ReferendumStatusOf, ScheduleAddressOf, + StringLike, TallyOf, Track, TrackIdOf, TrackInfo, TrackInfoOf, TracksInfo, VotesOf, }, weights::WeightInfo, }; @@ -130,12 +131,20 @@ pub mod pallet { }; /// The in-code storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); + /// A reason for holding funds. + /// Creates a hold reason for this pallet that is aggregated by `construct_runtime`. + #[pallet::composite_enum] + pub enum HoldReason { + /// The account has deposited tokens to place a decision deposit. + DecisionDeposit, + } + #[pallet::config] pub trait Config: frame_system::Config + Sized { // System level stuff. @@ -162,7 +171,11 @@ pub mod pallet { Hasher = Self::Hashing, >; /// Currency type for this pallet. - type Currency: ReservableCurrency; + type NativeBalance: Inspect + + Mutate + + Balanced + + InspectHold>> + + MutateHold>>; // Origins and unbalances. /// Origin from which proposals may be submitted. type SubmitOrigin: EnsureOriginWithArg< @@ -175,7 +188,7 @@ pub mod pallet { /// Origin from which any vote may be killed. type KillOrigin: EnsureOrigin; /// Handler for the unbalanced reduction when slashing a preimage deposit. - type Slash: OnUnbalanced>; + type Slash: OnUnbalanced>; /// The counting type for votes. Usually just balance. type Votes: AtLeast32BitUnsigned + Copy + Parameter + Member + MaxEncodedLen; /// The tallying type. @@ -569,7 +582,7 @@ pub mod pallet { .take_decision_deposit() .map_err(|_| Error::::Unfinished)? .ok_or(Error::::NoDeposit)?; - Self::refund_deposit(Some(deposit.clone())); + Self::refund_deposit(Some(deposit.clone()))?; ReferendumInfoFor::::insert(index, info); let e = Event::::DecisionDepositRefunded { index, @@ -621,8 +634,8 @@ pub mod pallet { } Self::note_one_fewer_deciding(status.track); Self::deposit_event(Event::::Killed { index, tally: status.tally }); - Self::slash_deposit(Some(status.submission_deposit.clone())); - Self::slash_deposit(status.decision_deposit.clone()); + Self::slash_deposit(Some(status.submission_deposit.clone()))?; + Self::slash_deposit(status.decision_deposit.clone())?; Self::do_clear_metadata(index); let info = ReferendumInfo::Killed(T::BlockNumberProvider::current_block_number()); ReferendumInfoFor::::insert(index, info); @@ -707,7 +720,7 @@ pub mod pallet { .take_submission_deposit() .map_err(|_| Error::::BadStatus)? .ok_or(Error::::NoDeposit)?; - Self::refund_deposit(Some(deposit.clone())); + Self::refund_deposit(Some(deposit.clone()))?; ReferendumInfoFor::::insert(index, info); let e = Event::::SubmissionDepositRefunded { index, @@ -1289,23 +1302,40 @@ impl, I: 'static> Pallet { who: T::AccountId, amount: BalanceOf, ) -> Result>, DispatchError> { - T::Currency::reserve(&who, amount)?; + T::NativeBalance::hold(&HoldReason::::DecisionDeposit.into(), &who, amount)?; Ok(Deposit { who, amount }) } /// Return a deposit, if `Some`. - fn refund_deposit(deposit: Option>>) { + fn refund_deposit( + deposit: Option>>, + ) -> Result<(), DispatchError> { if let Some(Deposit { who, amount }) = deposit { - T::Currency::unreserve(&who, amount); + T::NativeBalance::release( + &HoldReason::::DecisionDeposit.into(), + &who, + amount, + Precision::BestEffort, + )?; } + Ok(()) } /// Slash a deposit, if `Some`. - fn slash_deposit(deposit: Option>>) { + fn slash_deposit( + deposit: Option>>, + ) -> Result<(), DispatchError> { if let Some(Deposit { who, amount }) = deposit { - T::Slash::on_unbalanced(T::Currency::slash_reserved(&who, amount).0); - Self::deposit_event(Event::::DepositSlashed { who, amount }); + let amount_burned = T::NativeBalance::burn_held( + &HoldReason::::DecisionDeposit.into(), + &who, + amount, + Precision::BestEffort, + Fortitude::Force, + )?; + Self::deposit_event(Event::::DepositSlashed { who, amount: amount_burned }); } + Ok(()) } /// Determine whether the given `tally` would result in a referendum passing at `elapsed` blocks diff --git a/substrate/frame/referenda/src/migration.rs b/substrate/frame/referenda/src/migration.rs index 1b0f8a6e9faa8..aa06bd99c3f9c 100644 --- a/substrate/frame/referenda/src/migration.rs +++ b/substrate/frame/referenda/src/migration.rs @@ -180,6 +180,220 @@ pub mod v1 { } } +pub mod v2 { + use super::*; + use frame_support::traits::ReservableCurrency; + + /// The log target. + const TARGET: &'static str = "runtime::referenda::migration::v2"; + + /// Migrate from the old `Currency` reserve system to the new `fungible` hold system. + /// + /// This migration: + /// 1. Iterates through all referenda with deposits + /// 2. Unreserves the old reserved balance + /// 3. Places a hold with the new `HoldReason::DecisionDeposit` + pub struct MigrateV1ToV2(PhantomData<(T, I, OldCurrency)>); + + impl OnRuntimeUpgrade for MigrateV1ToV2 + where + T: Config, + I: 'static, + OldCurrency: ReservableCurrency>, + { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + let referendum_count = ReferendumInfoFor::::iter().count(); + log::info!( + target: TARGET, + "pre-upgrade state contains '{}' referendums.", + referendum_count + ); + + // Count deposits that need migration + let mut deposit_count = 0u32; + for (_, info) in ReferendumInfoFor::::iter() { + match info { + ReferendumInfo::Ongoing(status) => { + if status.submission_deposit.amount > Zero::zero() { + deposit_count += 1; + } + if let Some(ref d) = status.decision_deposit { + if d.amount > Zero::zero() { + deposit_count += 1; + } + } + }, + ReferendumInfo::Approved(_, ref s, ref d) | + ReferendumInfo::Rejected(_, ref s, ref d) | + ReferendumInfo::Cancelled(_, ref s, ref d) | + ReferendumInfo::TimedOut(_, ref s, ref d) => { + if let Some(ref submission) = s { + if submission.amount > Zero::zero() { + deposit_count += 1; + } + } + if let Some(ref decision) = d { + if decision.amount > Zero::zero() { + deposit_count += 1; + } + } + }, + ReferendumInfo::Killed(_) => {}, + } + } + + log::info!( + target: TARGET, + "pre-upgrade: '{}' deposits to migrate.", + deposit_count + ); + + Ok((referendum_count as u32, deposit_count).encode()) + } + + fn on_runtime_upgrade() -> Weight { + let in_code_version = Pallet::::in_code_storage_version(); + let on_chain_version = Pallet::::on_chain_storage_version(); + let mut weight = T::DbWeight::get().reads(1); + + log::info!( + target: TARGET, + "running migration with in-code storage version {:?} / onchain {:?}.", + in_code_version, + on_chain_version + ); + + if on_chain_version != 1 { + log::warn!(target: TARGET, "skipping migration from v1 to v2."); + return weight; + } + + let mut migrated_deposits = 0u32; + + for (index, info) in v1::ReferendumInfoFor::::iter() { + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + let deposits_to_migrate = Self::collect_deposits(&info); + + for Deposit { who, amount } in deposits_to_migrate { + if amount.is_zero() { + continue; + } + + // Unreserve the old reserved balance + let remaining = OldCurrency::unreserve(&who, amount); + if !remaining.is_zero() { + log::warn!( + target: TARGET, + "referendum #{:?}: could not fully unreserve for {:?}. Remaining: {:?}", + index, + who, + remaining + ); + } + + // Hold with the new HoldReason + let amount_to_hold = amount.saturating_sub(remaining); + if !amount_to_hold.is_zero() { + if let Err(e) = T::NativeBalance::hold( + &HoldReason::DecisionDeposit.into(), + &who, + amount_to_hold, + ) { + log::error!( + target: TARGET, + "referendum #{:?}: failed to hold {:?} for {:?}: {:?}", + index, + amount_to_hold, + who, + e + ); + } else { + migrated_deposits += 1; + log::info!( + target: TARGET, + "referendum #{:?}: migrated deposit of {:?} for {:?}", + index, + amount_to_hold, + who + ); + } + } + + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + } + } + + StorageVersion::new(2).put::>(); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + log::info!( + target: TARGET, + "migration complete. Migrated {} deposits.", + migrated_deposits + ); + + weight + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), TryRuntimeError> { + let on_chain_version = Pallet::::on_chain_storage_version(); + ensure!(on_chain_version == 2, "must upgrade from version 1 to 2."); + + let (pre_referendum_count, _pre_deposit_count): (u32, u32) = + Decode::decode(&mut &state[..]) + .expect("failed to decode the state from pre-upgrade."); + + let post_referendum_count = ReferendumInfoFor::::iter().count() as u32; + ensure!( + post_referendum_count == pre_referendum_count, + "referendum count must remain the same." + ); + + log::info!(target: TARGET, "migration verification complete."); + Ok(()) + } + } + + impl MigrateV1ToV2 + where + T: Config, + I: 'static, + { + /// Collect all deposits from a referendum that need migration. + fn collect_deposits( + info: &v1::ReferendumInfoOf, + ) -> Vec>> { + let mut deposits = Vec::new(); + + match info { + ReferendumInfo::Ongoing(status) => { + deposits.push(status.submission_deposit.clone()); + if let Some(ref d) = status.decision_deposit { + deposits.push(d.clone()); + } + }, + ReferendumInfo::Approved(_, ref s, ref d) | + ReferendumInfo::Rejected(_, ref s, ref d) | + ReferendumInfo::Cancelled(_, ref s, ref d) | + ReferendumInfo::TimedOut(_, ref s, ref d) => { + if let Some(ref submission) = s { + deposits.push(submission.clone()); + } + if let Some(ref decision) = d { + deposits.push(decision.clone()); + } + }, + ReferendumInfo::Killed(_) => {}, + } + + deposits + } + } +} + /// Migration for when changing the block number provider. /// /// This migration is not guarded @@ -300,6 +514,8 @@ pub mod test { mock::{Test as T, *}, }; use core::str::FromStr; + use frame_support::assert_ok; + use pallet_balances::Pallet as Balances; // create referendum status v0. fn create_status_v0() -> v0::ReferendumStatusOf { @@ -406,4 +622,207 @@ pub mod test { ); }); } + + #[test] + fn migration_v1_to_v2_works() { + use frame_support::traits::{fungible::InspectHold, Currency, ReservableCurrency}; + + ExtBuilder::default().build_and_execute(|| { + // Setup: Fund accounts and reserve balances (simulating v1 state) + let submitter: u64 = 1; + let decision_depositor: u64 = 2; + let submission_amount: u64 = 10; + let decision_amount: u64 = 20; + + // Give accounts enough balance - use fully qualified syntax + let _ = as Currency>::deposit_creating(&submitter, 1000); + let _ = as Currency>::deposit_creating(&decision_depositor, 1000); + + // Reserve funds using old Currency trait (simulating v1 state) + assert_ok!( as ReservableCurrency>::reserve( + &submitter, + submission_amount + )); + assert_ok!( as ReservableCurrency>::reserve( + &decision_depositor, + decision_amount + )); + + // Verify reserves are in place + assert_eq!( + as ReservableCurrency>::reserved_balance(&submitter), + submission_amount + ); + assert_eq!( + as ReservableCurrency>::reserved_balance(&decision_depositor), + decision_amount + ); + + // Create an ongoing referendum with both deposits + let status_v1 = create_status_v0(); + let ongoing_with_decision = v1::ReferendumInfoOf::::Ongoing(ReferendumStatus { + submission_deposit: Deposit { who: submitter, amount: submission_amount }, + decision_deposit: Some(Deposit { + who: decision_depositor, + amount: decision_amount, + }), + ..status_v1 + }); + + // Create an approved referendum with deposits + let approved_v1 = v1::ReferendumInfoOf::::Approved( + 100, + Some(Deposit { who: submitter, amount: submission_amount }), + Some(Deposit { who: decision_depositor, amount: decision_amount }), + ); + + // Create a timed out referendum with only submission deposit + let timed_out_v1 = v1::ReferendumInfoOf::::TimedOut( + 50, + Some(Deposit { who: submitter, amount: submission_amount }), + None, + ); + + // Reserve more for the additional referenda + assert_ok!( as ReservableCurrency>::reserve( + &submitter, + submission_amount * 2 + )); + assert_ok!( as ReservableCurrency>::reserve( + &decision_depositor, + decision_amount + )); + + // Insert referenda into storage + ReferendumCount::::put(3); + v1::ReferendumInfoFor::::insert(0, ongoing_with_decision); + v1::ReferendumInfoFor::::insert(1, approved_v1); + v1::ReferendumInfoFor::::insert(2, timed_out_v1); + + // Set storage version to 1 + StorageVersion::new(1).put::>(); + assert_eq!(Pallet::::on_chain_storage_version(), 1); + + // Verify total reserved before migration + let submitter_reserved_before = + as ReservableCurrency>::reserved_balance(&submitter); + let depositor_reserved_before = + as ReservableCurrency>::reserved_balance(&decision_depositor); + assert_eq!(submitter_reserved_before, submission_amount * 3); // 3 submission deposits + assert_eq!(depositor_reserved_before, decision_amount * 2); // 2 decision deposits + + // Verify no holds exist before migration + assert_eq!( + as InspectHold>::balance_on_hold( + &HoldReason::DecisionDeposit.into(), + &submitter + ), + 0u64 + ); + assert_eq!( + as InspectHold>::balance_on_hold( + &HoldReason::DecisionDeposit.into(), + &decision_depositor + ), + 0u64 + ); + + // Run migration + v2::MigrateV1ToV2::>::on_runtime_upgrade(); + + // Verify storage version updated to 2 + assert_eq!(Pallet::::on_chain_storage_version(), 2); + + // Verify holds are now in place with the correct reason + let submitter_held = as InspectHold>::balance_on_hold( + &HoldReason::DecisionDeposit.into(), + &submitter, + ); + let depositor_held = as InspectHold>::balance_on_hold( + &HoldReason::DecisionDeposit.into(), + &decision_depositor, + ); + + assert_eq!(submitter_held, submission_amount * 3); + assert_eq!(depositor_held, decision_amount * 2); + + // The reserved_balance will equal the held amount (holds use reserved storage) + assert_eq!( + as ReservableCurrency>::reserved_balance(&submitter), + submitter_held + ); + assert_eq!( + as ReservableCurrency>::reserved_balance(&decision_depositor), + depositor_held + ); + + // Verify referendum data is unchanged + let ref_0 = ReferendumInfoFor::::get(0).unwrap(); + let ref_1 = ReferendumInfoFor::::get(1).unwrap(); + let ref_2 = ReferendumInfoFor::::get(2).unwrap(); + + // Data should still contain the deposit information + match ref_0 { + ReferendumInfo::Ongoing(status) => { + assert_eq!(status.submission_deposit.amount, submission_amount); + assert_eq!(status.decision_deposit.unwrap().amount, decision_amount); + }, + _ => panic!("Expected Ongoing referendum"), + } + + match ref_1 { + ReferendumInfo::Approved(_, s, d) => { + assert_eq!(s.unwrap().amount, submission_amount); + assert_eq!(d.unwrap().amount, decision_amount); + }, + _ => panic!("Expected Approved referendum"), + } + + match ref_2 { + ReferendumInfo::TimedOut(_, s, d) => { + assert_eq!(s.unwrap().amount, submission_amount); + assert!(d.is_none()); + }, + _ => panic!("Expected TimedOut referendum"), + } + }); + } + + #[test] + fn migration_v1_to_v2_skips_if_not_v1() { + ExtBuilder::default().build_and_execute(|| { + // Set storage version to 0 (not 1) + StorageVersion::new(0).put::>(); + + // Run migration - should skip + v2::MigrateV1ToV2::>::on_runtime_upgrade(); + + // Version should remain at 0 + assert_eq!(Pallet::::on_chain_storage_version(), 0); + }); + } + + #[test] + fn migration_v1_to_v2_handles_killed_referendum() { + ExtBuilder::default().build_and_execute(|| { + // Create a killed referendum (no deposits to migrate) + let killed = ReferendumInfoOf::::Killed(42); + + ReferendumCount::::put(1); + v1::ReferendumInfoFor::::insert(0, killed); + StorageVersion::new(1).put::>(); + + // Run migration + v2::MigrateV1ToV2::>::on_runtime_upgrade(); + + // Should complete successfully + assert_eq!(Pallet::::on_chain_storage_version(), 2); + + // Referendum should still be Killed + match ReferendumInfoFor::::get(0).unwrap() { + ReferendumInfo::Killed(moment) => assert_eq!(moment, 42), + _ => panic!("Expected Killed referendum"), + } + }); + } } diff --git a/substrate/frame/referenda/src/mock.rs b/substrate/frame/referenda/src/mock.rs index 52a89d3f7cb7d..324e9c5fc61b0 100644 --- a/substrate/frame/referenda/src/mock.rs +++ b/substrate/frame/referenda/src/mock.rs @@ -196,7 +196,7 @@ impl Config for Test { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = pallet_balances::Pallet; + type NativeBalance = pallet_balances::Pallet; type SubmitOrigin = frame_system::EnsureSigned; type CancelOrigin = EnsureSignedBy; type KillOrigin = EnsureRoot; diff --git a/substrate/frame/referenda/src/tests.rs b/substrate/frame/referenda/src/tests.rs index d556d10c44a6b..0a2ce94b8907f 100644 --- a/substrate/frame/referenda/src/tests.rs +++ b/substrate/frame/referenda/src/tests.rs @@ -22,8 +22,7 @@ use crate::mock::{RefState::*, *}; use assert_matches::assert_matches; use codec::Decode; use frame_support::{assert_noop, assert_ok, dispatch::RawOrigin, traits::Contains}; -use pallet_balances::Error as BalancesError; -use sp_runtime::DispatchError::BadOrigin; +use sp_runtime::{DispatchError::BadOrigin, TokenError}; #[test] fn params_should_work() { @@ -409,7 +408,7 @@ fn submit_errors_work() { h, DispatchTime::At(10), ), - BalancesError::::InsufficientBalance + TokenError::FundsUnavailable ); }); } @@ -427,8 +426,10 @@ fn decision_deposit_errors_work() { h, DispatchTime::At(10), )); - let e = BalancesError::::InsufficientBalance; - assert_noop!(Referenda::place_decision_deposit(RuntimeOrigin::signed(10), 0), e); + assert_noop!( + Referenda::place_decision_deposit(RuntimeOrigin::signed(10), 0), + TokenError::FundsUnavailable + ); assert_ok!(Referenda::place_decision_deposit(RuntimeOrigin::signed(2), 0)); let e = Error::::HasDeposit; diff --git a/substrate/frame/referenda/src/types.rs b/substrate/frame/referenda/src/types.rs index caca9087f25a4..f8c3add42835b 100644 --- a/substrate/frame/referenda/src/types.rs +++ b/substrate/frame/referenda/src/types.rs @@ -22,7 +22,11 @@ use alloc::borrow::Cow; use codec::{Compact, Decode, DecodeWithMemTracking, Encode, EncodeLike, Input, MaxEncodedLen}; use core::fmt::Debug; use frame_support::{ - traits::{schedule::v3::Anon, Bounded}, + traits::{ + fungible::{Debt, Inspect}, + schedule::v3::Anon, + Bounded, + }, Parameter, }; use scale_info::{Type, TypeInfo}; @@ -30,14 +34,13 @@ use sp_arithmetic::{Rounding::*, SignedRounding::*}; use sp_runtime::{FixedI64, PerThing}; pub type BalanceOf = - <>::Currency as Currency<::AccountId>>::Balance; + <>::NativeBalance as Inspect<::AccountId>>::Balance; pub type BlockNumberFor = <>::BlockNumberProvider as BlockNumberProvider>::BlockNumber; -pub type NegativeImbalanceOf = <>::Currency as Currency< - ::AccountId, ->>::NegativeImbalance; +pub type DebtOf = + Debt<::AccountId, >::NativeBalance>; pub type CallOf = >::RuntimeCall; pub type BoundedCallOf = Bounded<>::RuntimeCall, ::Hashing>; diff --git a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs index 6ad74378e50b6..939baca9d505d 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -93,7 +93,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = Balances; + type NativeBalance = Balances; type SubmitOrigin = frame_system::EnsureSigned; type CancelOrigin = EitherOf, ReferendumCanceller>; type KillOrigin = EitherOf, ReferendumKiller>; diff --git a/substrate/frame/staking-async/runtimes/rc/src/governance/mod.rs b/substrate/frame/staking-async/runtimes/rc/src/governance/mod.rs index 47f1f8c2e98a7..75d9779ebc74d 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/governance/mod.rs @@ -83,7 +83,7 @@ impl pallet_referenda::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; type Scheduler = Scheduler; - type Currency = Balances; + type NativeBalance = Balances; type SubmitOrigin = frame_system::EnsureSigned; type CancelOrigin = EitherOf, ReferendumCanceller>; type KillOrigin = EitherOf, ReferendumKiller>;