diff --git a/pallets/funding/src/storage_migrations.rs b/pallets/funding/src/storage_migrations.rs index d426703ef..4b6042677 100644 --- a/pallets/funding/src/storage_migrations.rs +++ b/pallets/funding/src/storage_migrations.rs @@ -1,5 +1,334 @@ //! A module that is responsible for migration of storage. use frame_support::traits::StorageVersion; /// The current storage version -pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); +pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); pub const LOG: &str = "runtime::funding::migration"; + +pub mod v5 { + use crate::{ + AccountIdOf, BalanceOf, BlockNumberPair, CheckOutcome, Config, EvaluationRoundInfoOf, EvaluatorsOutcome, + FixedPointNumber, FundingOutcome, HRMPChannelStatus, Pallet, PriceOf, ProjectDetailsOf, ProjectStatus, + RewardInfo, + }; + use core::marker::PhantomData; + use frame_support::traits::{tokens::Balance as BalanceT, UncheckedOnRuntimeUpgrade}; + use frame_system::pallet_prelude::BlockNumberFor; + use polimec_common::credentials::Did; + use polkadot_parachain_primitives::primitives::Id as ParaId; + use scale_info::TypeInfo; + use serde::{Deserialize, Serialize}; + use sp_core::{Decode, Encode, Get, MaxEncodedLen, RuntimeDebug}; + use sp_runtime::traits::One; + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] + pub struct OldProjectDetails< + AccountId, + Did, + BlockNumber, + Price: FixedPointNumber, + Balance: BalanceT, + EvaluationRoundInfo, + > { + pub issuer_account: AccountId, + pub issuer_did: Did, + /// Whether the project is frozen, so no `metadata` changes are allowed. + pub is_frozen: bool, + /// The price in USD per token decided after the Auction Round + pub weighted_average_price: Option, + /// The current status of the project + pub status: OldProjectStatus, + /// When the different project phases start and end + pub phase_transition_points: OldPhaseTransitionPoints, + /// Fundraising target amount in USD (6 decimals) + pub fundraising_target_usd: Balance, + /// The amount of Contribution Tokens that have not yet been sold + pub remaining_contribution_tokens: Balance, + /// Funding reached amount in USD (6 decimals) + pub funding_amount_reached_usd: Balance, + /// Information about the total amount bonded, and the outcome in regards to reward/slash/nothing + pub evaluation_round_info: EvaluationRoundInfo, + /// If the auction was oversubscribed, how much USD was raised across all winning bids + pub usd_bid_on_oversubscription: Option, + /// When the Funding Round ends + pub funding_end_block: Option, + pub migration_type: Option, + } + #[derive( + Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Serialize, Deserialize, + )] + pub enum OldProjectStatus { + #[default] + Application, + EvaluationRound, + AuctionInitializePeriod, + AuctionOpening, + AuctionClosing, + CalculatingWAP, + CommunityRound, + RemainderRound, + FundingFailed, + AwaitingProjectDecision, + FundingSuccessful, + SettlementStarted(OldFundingOutcome), + SettlementFinished(OldFundingOutcome), + CTMigrationStarted, + CTMigrationFinished, + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Serialize, Deserialize)] + pub enum OldFundingOutcome { + FundingSuccessful, + FundingFailed, + } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] + pub struct OldPhaseTransitionPoints { + pub application: OldBlockNumberPair, + pub evaluation: OldBlockNumberPair, + pub auction_initialize_period: OldBlockNumberPair, + pub auction_opening: OldBlockNumberPair, + pub random_closing_ending: Option, + pub auction_closing: OldBlockNumberPair, + pub community: OldBlockNumberPair, + pub remainder: OldBlockNumberPair, + } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] + pub struct OldBlockNumberPair { + pub start: Option, + pub end: Option, + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub struct OldEvaluationRoundInfo { + pub total_bonded_usd: Balance, + pub total_bonded_plmc: Balance, + pub evaluators_outcome: OldEvaluatorsOutcome, + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum OldEvaluatorsOutcome { + Unchanged, + Rewarded(RewardInfo), + Slashed, + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] + pub enum OldMigrationType { + Offchain, + Pallet(OldPalletMigrationInfo), + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] + pub struct OldPalletMigrationInfo { + /// ParaId of project + pub parachain_id: ParaId, + /// HRMP Channel status + pub hrmp_channel_status: HRMPChannelStatus, + /// Migration readiness check + pub migration_readiness_check: Option, + } + + #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub struct OldPalletMigrationReadinessCheck { + pub holding_check: (xcm::v3::QueryId, CheckOutcome), + pub pallet_check: (xcm::v3::QueryId, CheckOutcome), + } + + type OldProjectDetailsOf = OldProjectDetails< + AccountIdOf, + Did, + BlockNumberFor, + PriceOf, + BalanceOf, + OldEvaluationRoundInfo>, + >; + + pub struct UncheckedMigrationToV5(PhantomData); + impl UncheckedOnRuntimeUpgrade for UncheckedMigrationToV5 { + fn on_runtime_upgrade() -> frame_support::weights::Weight { + let mut items = 0; + log::info!("Starting migration to V5"); + let mut translate_project_details = |key, item: OldProjectDetailsOf| -> Option> { + items += 1; + log::info!("project_details item {:?}", items); + let round_duration: BlockNumberPair>; + let new_status = match item.status { + OldProjectStatus::Application => { + let start = + item.phase_transition_points.application.start.expect("Application start block is missing"); + let end = + item.phase_transition_points.application.end.expect("Application end block is missing"); + round_duration = BlockNumberPair::new(Some(start), Some(end)); + ProjectStatus::Application + }, + OldProjectStatus::EvaluationRound => { + let start = + item.phase_transition_points.evaluation.start.expect("Evaluation start block is missing"); + let end = item.phase_transition_points.evaluation.end.expect("Evaluation end block is missing"); + round_duration = BlockNumberPair::new(Some(start), Some(end)); + ProjectStatus::EvaluationRound + }, + OldProjectStatus::AuctionInitializePeriod => { + let now = frame_system::Pallet::::block_number(); + let start = now; + let end = now + ::AuctionRoundDuration::get(); + round_duration = BlockNumberPair::new(Some(start), Some(end)); + debug_assert!(false, "AuctionInitializePeriod is not supported in V5, no project should be in this state when upgrading storage"); + log::error!("AuctionInitializePeriod is not supported in V5, no project should be in this state when upgrading storage"); + ProjectStatus::AuctionRound + }, + OldProjectStatus::AuctionOpening => { + let start = item + .phase_transition_points + .auction_opening + .start + .expect("AuctionOpening start block is missing"); + let end = start + ::AuctionRoundDuration::get(); + round_duration = BlockNumberPair::new(Some(start), Some(end)); + debug_assert!(false, "AuctionOpening is not supported in V5, no project should be in this state when upgrading storage"); + log::error!("AuctionOpening is not supported in V5, no project should be in this state when upgrading storage"); + ProjectStatus::AuctionRound + }, + OldProjectStatus::AuctionClosing => { + let start = item + .phase_transition_points + .auction_opening + .start + .expect("AuctionOpening start block is missing"); + let end = start + ::AuctionRoundDuration::get(); + round_duration = BlockNumberPair::new(Some(start), Some(end)); + debug_assert!(false, "AuctionClosing is not supported in V5, no project should be in this state when upgrading storage"); + log::error!("AuctionClosing is not supported in V5, no project should be in this state when upgrading storage"); + ProjectStatus::AuctionRound + }, + OldProjectStatus::CalculatingWAP => { + let start = item + .phase_transition_points + .auction_opening + .start + .expect("AuctionOpening start block is missing"); + let end = start + ::AuctionRoundDuration::get(); + round_duration = BlockNumberPair::new(Some(start), Some(end)); + debug_assert!(false, "CalculatingWAP is not supported in V5, no project should be in this state when upgrading storage"); + log::error!("CalculatingWAP is not supported in V5, no project should be in this state when upgrading storage"); + ProjectStatus::AuctionRound + }, + OldProjectStatus::CommunityRound => { + let start = item + .phase_transition_points + .community + .start + .expect("CommunityRound start block is missing"); + let end = start + + ::CommunityRoundDuration::get() + + ::RemainderRoundDuration::get(); + round_duration = BlockNumberPair::new(Some(start), Some(end)); + debug_assert!( + false, + "We should not upgrade runtime while a project is still in community round" + ); + ProjectStatus::CommunityRound( + item.phase_transition_points.community.end.expect("CommunityRound end block is missing") + + One::one(), + ) + }, + OldProjectStatus::RemainderRound => { + let start = item + .phase_transition_points + .community + .start + .expect("CommunityRound start block is missing"); + let end = start + + ::CommunityRoundDuration::get() + + ::RemainderRoundDuration::get(); + round_duration = BlockNumberPair::new(Some(start), Some(end)); + ProjectStatus::CommunityRound( + item.phase_transition_points.remainder.start.expect("Remainder start block is missing"), + ) + }, + OldProjectStatus::FundingFailed => { + round_duration = BlockNumberPair::new(None, None); + ProjectStatus::SettlementStarted(FundingOutcome::Failure) + }, + OldProjectStatus::AwaitingProjectDecision => { + round_duration = BlockNumberPair::new(None, None); + debug_assert!(false, "AwaitingProjectDecision is not supported in V5, no project should be in this state when upgrading storage"); + log::error!("AwaitingProjectDecision is not supported in V5, no project should be in this state when upgrading storage"); + ProjectStatus::FundingSuccessful + }, + OldProjectStatus::FundingSuccessful => { + round_duration = BlockNumberPair::new(None, None); + ProjectStatus::SettlementStarted(FundingOutcome::Success) + }, + OldProjectStatus::SettlementStarted(old_outcome) => { + round_duration = BlockNumberPair::new(None, None); + let outcome = match old_outcome { + OldFundingOutcome::FundingSuccessful => FundingOutcome::Success, + OldFundingOutcome::FundingFailed => FundingOutcome::Failure, + }; + ProjectStatus::SettlementStarted(outcome) + }, + OldProjectStatus::SettlementFinished(old_outcome) => { + round_duration = BlockNumberPair::new(None, None); + let outcome = match old_outcome { + OldFundingOutcome::FundingSuccessful => FundingOutcome::Success, + OldFundingOutcome::FundingFailed => FundingOutcome::Failure, + }; + ProjectStatus::SettlementFinished(outcome) + }, + OldProjectStatus::CTMigrationStarted => { + round_duration = BlockNumberPair::new(None, None); + ProjectStatus::CTMigrationStarted + }, + OldProjectStatus::CTMigrationFinished => { + round_duration = BlockNumberPair::new(None, None); + ProjectStatus::CTMigrationFinished + }, + }; + + let evaluators_outcome = Some(match item.evaluation_round_info.evaluators_outcome { + OldEvaluatorsOutcome::Unchanged => EvaluatorsOutcome::Rewarded( + >::generate_evaluator_rewards_info(key) + .expect("Evaluator rewards info should be generated"), + ), + OldEvaluatorsOutcome::Rewarded(info) => EvaluatorsOutcome::>::Rewarded(info), + OldEvaluatorsOutcome::Slashed => EvaluatorsOutcome::>::Slashed, + }); + let evaluation_round_info = EvaluationRoundInfoOf:: { + total_bonded_usd: item.evaluation_round_info.total_bonded_usd, + total_bonded_plmc: item.evaluation_round_info.total_bonded_plmc, + evaluators_outcome, + }; + Some(ProjectDetailsOf:: { + issuer_account: item.issuer_account, + issuer_did: item.issuer_did, + is_frozen: item.is_frozen, + weighted_average_price: item.weighted_average_price, + status: new_status, + round_duration, + fundraising_target_usd: item.fundraising_target_usd, + remaining_contribution_tokens: item.remaining_contribution_tokens, + funding_amount_reached_usd: item.funding_amount_reached_usd, + evaluation_round_info, + usd_bid_on_oversubscription: item.usd_bid_on_oversubscription, + funding_end_block: item.funding_end_block, + migration_type: None, + }) + }; + crate::ProjectsDetails::::translate(|key, object: OldProjectDetailsOf| { + translate_project_details(key, object) + }); + + T::DbWeight::get().reads_writes(items, items) + } + } + + pub type MigrationToV5 = frame_support::migrations::VersionedMigration< + 4, + 5, + UncheckedMigrationToV5, + crate::Pallet, + ::DbWeight, + >; +} diff --git a/pallets/funding/src/tests/4_contribution.rs b/pallets/funding/src/tests/4_contribution.rs index 275a526ef..18e1157a2 100644 --- a/pallets/funding/src/tests/4_contribution.rs +++ b/pallets/funding/src/tests/4_contribution.rs @@ -1758,14 +1758,17 @@ mod contribute_extrinsic { let glutton_contribution = ContributionParams::new(BUYER_1, remaining_cts, 4u8, AcceptedFundingAsset::USDT); let wap = project_details.weighted_average_price.unwrap(); let plmc_mint = inst.calculate_contributed_plmc_spent(vec![glutton_contribution.clone()], wap, true); - let funding_asset_mint = inst.calculate_contributed_funding_asset_spent(vec![glutton_contribution.clone()], wap); + let funding_asset_mint = + inst.calculate_contributed_funding_asset_spent(vec![glutton_contribution.clone()], wap); inst.mint_plmc_to(plmc_mint); inst.mint_funding_asset_to(funding_asset_mint); inst.contribute_for_users(project_id, vec![glutton_contribution.clone()]).unwrap(); - let failing_contribution = ContributionParams::::new(BUYER_2, 1000 * CT_UNIT, 1u8, AcceptedFundingAsset::USDT); + let failing_contribution = + ContributionParams::::new(BUYER_2, 1000 * CT_UNIT, 1u8, AcceptedFundingAsset::USDT); let plmc_mint = inst.calculate_contributed_plmc_spent(vec![glutton_contribution.clone()], wap, true); - let funding_asset_mint = inst.calculate_contributed_funding_asset_spent(vec![glutton_contribution.clone()], wap); + let funding_asset_mint = + inst.calculate_contributed_funding_asset_spent(vec![glutton_contribution.clone()], wap); inst.mint_plmc_to(plmc_mint); inst.mint_funding_asset_to(funding_asset_mint); inst.execute(|| { diff --git a/runtimes/polimec/src/custom_migrations/funding_holds.rs b/runtimes/polimec/src/custom_migrations/funding_holds.rs new file mode 100644 index 000000000..dba8c7526 --- /dev/null +++ b/runtimes/polimec/src/custom_migrations/funding_holds.rs @@ -0,0 +1,91 @@ +use crate::{Balance, Funding, Runtime, RuntimeHoldReason}; +use alloc::vec::Vec; +use frame_support::traits::{GetStorageVersion, OnRuntimeUpgrade, VariantCount, VariantCountOf}; +use pallet_balances::IdAmount; +use pallet_funding::ProjectId; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_core::{MaxEncodedLen, RuntimeDebug}; +use sp_runtime::BoundedVec; + +#[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum OldFundingHoldReason { + Evaluation(ProjectId), + Participation(ProjectId), +} + +#[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum OldRuntimeHoldReason { + #[codec(index = 25u8)] + ParachainStaking(pallet_parachain_staking::HoldReason), + + #[codec(index = 41u8)] + Democracy(pallet_democracy::HoldReason), + + #[codec(index = 44u8)] + Elections(pallet_elections_phragmen::HoldReason), + + #[codec(index = 45u8)] + Preimage(pallet_preimage::HoldReason), + + #[codec(index = 80u8)] + Funding(OldFundingHoldReason), +} + +impl VariantCount for OldRuntimeHoldReason { + const VARIANT_COUNT: u32 = 2 + 1 + 1 + 1 + 2; +} + +type OldIdAmount = IdAmount; +type NewIdAmount = IdAmount; +type OldHoldsItem = BoundedVec>; +type NewHoldsItem = BoundedVec>; + +pub struct FromFundingV4Migration; +impl OnRuntimeUpgrade for FromFundingV4Migration { + fn on_runtime_upgrade() -> frame_support::weights::Weight { + let on_chain_version = Funding::on_chain_storage_version(); + if on_chain_version != 4 { + log::warn!("Funding Holds migration can be removed. Skipping it now...",); + return ::DbWeight::get().reads(1) + } + let mut items = 0; + let mut translate = |_key, old_user_holds: OldHoldsItem| -> Option { + items += 1; + log::info!("Migrating hold {:?}", items); + let mut new_user_holds = Vec::new(); + for user_hold in old_user_holds.iter() { + let new_id = match user_hold.id { + OldRuntimeHoldReason::ParachainStaking(reason) => RuntimeHoldReason::ParachainStaking(reason), + OldRuntimeHoldReason::Democracy(reason) => RuntimeHoldReason::Democracy(reason), + OldRuntimeHoldReason::Elections(reason) => RuntimeHoldReason::Elections(reason), + OldRuntimeHoldReason::Preimage(reason) => RuntimeHoldReason::Preimage(reason), + OldRuntimeHoldReason::Funding(OldFundingHoldReason::Evaluation(_)) => + RuntimeHoldReason::Funding(pallet_funding::HoldReason::Evaluation), + OldRuntimeHoldReason::Funding(OldFundingHoldReason::Participation(_)) => + RuntimeHoldReason::Funding(pallet_funding::HoldReason::Participation), + }; + new_user_holds.push(IdAmount { id: new_id, amount: user_hold.amount }) + } + let output = NewHoldsItem::try_from(new_user_holds); + + debug_assert!(output.is_ok(), "Failed to convert holds"); + if let Err(err) = &output { + log::error!( + "Holds conversion failed with err {:?} for the following user holds: {:?}", + err, + old_user_holds + ); + } + // If we failed to convert the holds, we delete them + output.ok() + }; + + pallet_balances::Holds::::translate(|key, object: OldHoldsItem| translate(key, object)); + + log::info!("Number of users migrated: {}", items); + let weight = ::DbWeight::get().reads_writes(items, items); + log::info!("holds weight: {:?}", weight); + weight + } +} diff --git a/runtimes/polimec/src/custom_migrations/mod.rs b/runtimes/polimec/src/custom_migrations/mod.rs index 9ba6e3fb6..1d2de6e99 100644 --- a/runtimes/polimec/src/custom_migrations/mod.rs +++ b/runtimes/polimec/src/custom_migrations/mod.rs @@ -16,3 +16,5 @@ // the generated files do not pass clippy #![allow(clippy::all)] + +pub mod funding_holds; diff --git a/runtimes/polimec/src/lib.rs b/runtimes/polimec/src/lib.rs index 6d57a77ed..238aa8d01 100644 --- a/runtimes/polimec/src/lib.rs +++ b/runtimes/polimec/src/lib.rs @@ -162,10 +162,15 @@ pub type Migrations = migrations::Unreleased; /// The runtime migrations per release. #[allow(missing_docs)] pub mod migrations { + use crate::Runtime; /// Unreleased migrations. Add new ones here: #[allow(unused_parens)] - pub type Unreleased = (); + pub type Unreleased = ( + cumulus_pallet_xcmp_queue::migration::v5::MigrateV4ToV5, + crate::custom_migrations::funding_holds::FromFundingV4Migration, + pallet_funding::storage_migrations::v5::MigrationToV5, + ); } /// Executive: handles dispatch to the various modules. @@ -211,10 +216,10 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("polimec-mainnet"), impl_name: create_runtime_str!("polimec-mainnet"), authoring_version: 1, - spec_version: 0_007_006, + spec_version: 0_008_000, impl_version: 0, apis: RUNTIME_API_VERSIONS, - transaction_version: 4, + transaction_version: 5, state_version: 1, }; diff --git a/runtimes/polimec/src/xcm_config.rs b/runtimes/polimec/src/xcm_config.rs index a1c0cebbb..852035f65 100644 --- a/runtimes/polimec/src/xcm_config.rs +++ b/runtimes/polimec/src/xcm_config.rs @@ -470,3 +470,8 @@ impl> ShouldExecute for AllowHrmpNotifications { Ok(()) } } + +impl cumulus_pallet_xcmp_queue::migration::v5::V5Config for Runtime { + // This must be the same as the `ChannelInfo` from the `Config`: + type ChannelList = ParachainSystem; +} diff --git a/scripts/chopsticks/genesis-polimec.yml b/scripts/chopsticks/genesis-polimec.yml new file mode 100644 index 000000000..45c4117ae --- /dev/null +++ b/scripts/chopsticks/genesis-polimec.yml @@ -0,0 +1,34 @@ +db: ./db.sqlite +mock-signature-host: true +genesis: ./chain-specs/paseo/polimec-paseo.spec.raw.json + +import-storage: + System: + Account: + - - - 5EYCAe5ij8xKJ2WTFRZeeUsfaED5wz6z5XFv5LUw9Ni7VCea # Fund the Dispenser account + - providers: 1 + data: + free: '230000000000000000' + - - - '0xba143e2096e073cb9cddc78e6f4969d8a02160d716a69e08214caf5339d88c42' # Fund the Asset Owner account + - providers: 1 + data: + free: '10000000000000000' + - - - '5CojJLdz8ers6HBoEo7avwupYhZavXjTmCsUAe8w6aYcasq4' # Fund Felix account with PLMC + - providers: 1 + data: + free: '320000000000000' + ForeignAssets: + Account: [ + [ + [10, 5CojJLdz8ers6HBoEo7avwupYhZavXjTmCsUAe8w6aYcasq4], + { balance: 200000000000000 }, # Give to Felix 20k DOT + ], + [ + [1337, 5CojJLdz8ers6HBoEo7avwupYhZavXjTmCsUAe8w6aYcasq4], + { balance: 330000000000 }, # Give to Felix 33k USDC + ], + [ + [1984, 5CojJLdz8ers6HBoEo7avwupYhZavXjTmCsUAe8w6aYcasq4], + { balance: 1000000000 }, # Give to Felix 100k USDT + ], + ]