diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs
index 074dd9224df57279f297bf40a79c19d9000e9e81..8ef4d7151164822cfbe645666726794d3b1c4b4a 100644
--- a/polkadot/runtime/westend/src/lib.rs
+++ b/polkadot/runtime/westend/src/lib.rs
@@ -1705,6 +1705,8 @@ pub mod migrations {
 			MaxPoolsToMigrate,
 		>,
 		pallet_staking::migrations::v15::MigrateV14ToV15<Runtime>,
+		// permanent
+		pallet_xcm::migration::MigrateToLatestXcmVersion<Runtime>,
 	);
 }
 
diff --git a/polkadot/xcm/docs/src/cookbook/relay_token_transactor/parachain/xcm_config.rs b/polkadot/xcm/docs/src/cookbook/relay_token_transactor/parachain/xcm_config.rs
index 99f17693093e7f0472d78caf54f842847a8a3e84..6b7db0d709e2b08560d2cc9c33fb0216ec42dba5 100644
--- a/polkadot/xcm/docs/src/cookbook/relay_token_transactor/parachain/xcm_config.rs
+++ b/polkadot/xcm/docs/src/cookbook/relay_token_transactor/parachain/xcm_config.rs
@@ -168,7 +168,7 @@ impl pallet_xcm::Config for Runtime {
 	type UniversalLocation = UniversalLocation;
 	// No version discovery needed
 	const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 0;
-	type AdvertisedXcmVersion = frame::traits::ConstU32<3>;
+	type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion;
 	type AdminOrigin = frame_system::EnsureRoot<AccountId>;
 	// No locking
 	type TrustedLockers = ();
diff --git a/polkadot/xcm/docs/src/cookbook/relay_token_transactor/relay_chain/xcm_config.rs b/polkadot/xcm/docs/src/cookbook/relay_token_transactor/relay_chain/xcm_config.rs
index 987bb3f9ab6649bc299edafa97dc1d06166db440..6de71634d6b2697ec6582e7004c10751fe2be841 100644
--- a/polkadot/xcm/docs/src/cookbook/relay_token_transactor/relay_chain/xcm_config.rs
+++ b/polkadot/xcm/docs/src/cookbook/relay_token_transactor/relay_chain/xcm_config.rs
@@ -142,7 +142,7 @@ impl pallet_xcm::Config for Runtime {
 	type UniversalLocation = UniversalLocation;
 	// No version discovery needed
 	const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 0;
-	type AdvertisedXcmVersion = frame::traits::ConstU32<3>;
+	type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion;
 	type AdminOrigin = frame_system::EnsureRoot<AccountId>;
 	// No locking
 	type TrustedLockers = ();
diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs
index 05d9046ab192ae160c68b10f865ce587bb9bd4e0..f660ae6ff26c136b8a575e6b3a0de2e4926e5e1e 100644
--- a/polkadot/xcm/pallet-xcm/src/lib.rs
+++ b/polkadot/xcm/pallet-xcm/src/lib.rs
@@ -2692,6 +2692,44 @@ impl<T: Config> Pallet<T> {
 	/// set.
 	#[cfg(any(feature = "try-runtime", test))]
 	pub fn do_try_state() -> Result<(), TryRuntimeError> {
+		use migration::data::NeedsMigration;
+
+		// Take the minimum version between `SafeXcmVersion` and `latest - 1` and ensure that the
+		// operational data is stored at least at that version, for example, to prevent issues when
+		// removing older XCM versions.
+		let minimal_allowed_xcm_version = if let Some(safe_xcm_version) = SafeXcmVersion::<T>::get()
+		{
+			XCM_VERSION.saturating_sub(1).min(safe_xcm_version)
+		} else {
+			XCM_VERSION.saturating_sub(1)
+		};
+
+		// check `Queries`
+		ensure!(
+			!Queries::<T>::iter_values()
+				.any(|data| data.needs_migration(minimal_allowed_xcm_version)),
+			TryRuntimeError::Other("`Queries` data should be migrated to the higher xcm version!")
+		);
+
+		// check `LockedFungibles`
+		ensure!(
+			!LockedFungibles::<T>::iter_values()
+				.any(|data| data.needs_migration(minimal_allowed_xcm_version)),
+			TryRuntimeError::Other(
+				"`LockedFungibles` data should be migrated to the higher xcm version!"
+			)
+		);
+
+		// check `RemoteLockedFungibles`
+		ensure!(
+			!RemoteLockedFungibles::<T>::iter()
+				.any(|(key, data)| key.needs_migration(minimal_allowed_xcm_version) ||
+					data.needs_migration(minimal_allowed_xcm_version)),
+			TryRuntimeError::Other(
+				"`RemoteLockedFungibles` data should be migrated to the higher xcm version!"
+			)
+		);
+
 		// if migration has been already scheduled, everything is ok and data will be eventually
 		// migrated
 		if CurrentMigration::<T>::exists() {
@@ -2772,7 +2810,7 @@ impl<T: Config> xcm_executor::traits::Enact for UnlockTicket<T> {
 		let mut maybe_remove_index = None;
 		let mut locked = BalanceOf::<T>::zero();
 		let mut found = false;
-		// We could just as well do with with an into_iter, filter_map and collect, however this way
+		// We could just as well do with an into_iter, filter_map and collect, however this way
 		// avoids making an allocation.
 		for (i, x) in locks.iter_mut().enumerate() {
 			if x.1.try_as::<_>().defensive() == Ok(&self.unlocker) {
@@ -3154,7 +3192,7 @@ impl<T: Config> OnResponse for Pallet<T> {
 					});
 					return Weight::zero()
 				}
-				return match maybe_notify {
+				match maybe_notify {
 					Some((pallet_index, call_index)) => {
 						// This is a bit horrible, but we happen to know that the `Call` will
 						// be built by `(pallet_index: u8, call_index: u8, QueryId, Response)`.
diff --git a/polkadot/xcm/pallet-xcm/src/migration.rs b/polkadot/xcm/pallet-xcm/src/migration.rs
index 0aec97ab410516c35860f397d41ede599768c5c6..ccb6f7928540d064a541767ffcdf7c0a2c947108 100644
--- a/polkadot/xcm/pallet-xcm/src/migration.rs
+++ b/polkadot/xcm/pallet-xcm/src/migration.rs
@@ -15,7 +15,8 @@
 // along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
 
 use crate::{
-	pallet::CurrentMigration, Config, Pallet, VersionMigrationStage, VersionNotifyTargets,
+	pallet::CurrentMigration, Config, CurrentXcmVersion, Pallet, VersionMigrationStage,
+	VersionNotifyTargets,
 };
 use frame_support::{
 	pallet_prelude::*,
@@ -25,6 +26,307 @@ use frame_support::{
 
 const DEFAULT_PROOF_SIZE: u64 = 64 * 1024;
 
+/// Utilities for handling XCM version migration for the relevant data.
+pub mod data {
+	use crate::*;
+
+	/// A trait for handling XCM versioned data migration for the requested `XcmVersion`.
+	pub(crate) trait NeedsMigration {
+		type MigratedData;
+
+		/// Returns true if data does not match `minimal_allowed_xcm_version`.
+		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool;
+
+		/// Attempts to migrate data. `Ok(None)` means no migration is needed.
+		/// `Ok(Some(Self::MigratedData))` should contain the migrated data.
+		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()>;
+	}
+
+	/// Implementation of `NeedsMigration` for `LockedFungibles` data.
+	impl<B, M> NeedsMigration for BoundedVec<(B, VersionedLocation), M> {
+		type MigratedData = Self;
+
+		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
+			self.iter()
+				.any(|(_, unlocker)| unlocker.identify_version() < minimal_allowed_xcm_version)
+		}
+
+		fn try_migrate(
+			mut self,
+			to_xcm_version: XcmVersion,
+		) -> Result<Option<Self::MigratedData>, ()> {
+			let mut was_modified = false;
+			for locked in self.iter_mut() {
+				if locked.1.identify_version() < to_xcm_version {
+					let Ok(new_unlocker) = locked.1.clone().into_version(to_xcm_version) else {
+						return Err(())
+					};
+					locked.1 = new_unlocker;
+					was_modified = true;
+				}
+			}
+
+			if was_modified {
+				Ok(Some(self))
+			} else {
+				Ok(None)
+			}
+		}
+	}
+
+	/// Implementation of `NeedsMigration` for `Queries` data.
+	impl<BlockNumber> NeedsMigration for QueryStatus<BlockNumber> {
+		type MigratedData = Self;
+
+		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
+			match &self {
+				QueryStatus::Pending { responder, maybe_match_querier, .. } =>
+					responder.identify_version() < minimal_allowed_xcm_version ||
+						maybe_match_querier
+							.as_ref()
+							.map(|v| v.identify_version() < minimal_allowed_xcm_version)
+							.unwrap_or(false),
+				QueryStatus::VersionNotifier { origin, .. } =>
+					origin.identify_version() < minimal_allowed_xcm_version,
+				QueryStatus::Ready { response, .. } =>
+					response.identify_version() < minimal_allowed_xcm_version,
+			}
+		}
+
+		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
+			if !self.needs_migration(to_xcm_version) {
+				return Ok(None)
+			}
+
+			// do migration
+			match self {
+				QueryStatus::Pending { responder, maybe_match_querier, maybe_notify, timeout } => {
+					let Ok(responder) = responder.into_version(to_xcm_version) else {
+						return Err(())
+					};
+					let Ok(maybe_match_querier) =
+						maybe_match_querier.map(|mmq| mmq.into_version(to_xcm_version)).transpose()
+					else {
+						return Err(())
+					};
+					Ok(Some(QueryStatus::Pending {
+						responder,
+						maybe_match_querier,
+						maybe_notify,
+						timeout,
+					}))
+				},
+				QueryStatus::VersionNotifier { origin, is_active } => origin
+					.into_version(to_xcm_version)
+					.map(|origin| Some(QueryStatus::VersionNotifier { origin, is_active })),
+				QueryStatus::Ready { response, at } => response
+					.into_version(to_xcm_version)
+					.map(|response| Some(QueryStatus::Ready { response, at })),
+			}
+		}
+	}
+
+	/// Implementation of `NeedsMigration` for `RemoteLockedFungibles` key type.
+	impl<A> NeedsMigration for (XcmVersion, A, VersionedAssetId) {
+		type MigratedData = Self;
+
+		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
+			self.0 < minimal_allowed_xcm_version ||
+				self.2.identify_version() < minimal_allowed_xcm_version
+		}
+
+		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
+			if !self.needs_migration(to_xcm_version) {
+				return Ok(None)
+			}
+
+			let Ok(asset_id) = self.2.into_version(to_xcm_version) else { return Err(()) };
+			Ok(Some((to_xcm_version, self.1, asset_id)))
+		}
+	}
+
+	/// Implementation of `NeedsMigration` for `RemoteLockedFungibles` data.
+	impl<ConsumerIdentifier, MaxConsumers: Get<u32>> NeedsMigration
+		for RemoteLockedFungibleRecord<ConsumerIdentifier, MaxConsumers>
+	{
+		type MigratedData = Self;
+
+		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
+			self.owner.identify_version() < minimal_allowed_xcm_version ||
+				self.locker.identify_version() < minimal_allowed_xcm_version
+		}
+
+		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
+			if !self.needs_migration(to_xcm_version) {
+				return Ok(None)
+			}
+
+			let RemoteLockedFungibleRecord { amount, owner, locker, consumers } = self;
+
+			let Ok(owner) = owner.into_version(to_xcm_version) else { return Err(()) };
+			let Ok(locker) = locker.into_version(to_xcm_version) else { return Err(()) };
+
+			Ok(Some(RemoteLockedFungibleRecord { amount, owner, locker, consumers }))
+		}
+	}
+
+	impl<T: Config> Pallet<T> {
+		/// Migrates relevant data to the `required_xcm_version`.
+		pub(crate) fn migrate_data_to_xcm_version(
+			weight: &mut Weight,
+			required_xcm_version: XcmVersion,
+		) {
+			const LOG_TARGET: &str = "runtime::xcm::pallet_xcm::migrate_data_to_xcm_version";
+
+			// check and migrate `Queries`
+			let queries_to_migrate = Queries::<T>::iter().filter_map(|(id, data)| {
+				weight.saturating_add(T::DbWeight::get().reads(1));
+				match data.try_migrate(required_xcm_version) {
+					Ok(Some(new_data)) => Some((id, new_data)),
+					Ok(None) => None,
+					Err(_) => {
+						tracing::error!(
+							target: LOG_TARGET,
+							?id,
+							?required_xcm_version,
+							"`Queries` cannot be migrated!"
+						);
+						None
+					},
+				}
+			});
+			for (id, new_data) in queries_to_migrate {
+				tracing::info!(
+					target: LOG_TARGET,
+					query_id = ?id,
+					?new_data,
+					"Migrating `Queries`"
+				);
+				Queries::<T>::insert(id, new_data);
+				weight.saturating_add(T::DbWeight::get().writes(1));
+			}
+
+			// check and migrate `LockedFungibles`
+			let locked_fungibles_to_migrate =
+				LockedFungibles::<T>::iter().filter_map(|(id, data)| {
+					weight.saturating_add(T::DbWeight::get().reads(1));
+					match data.try_migrate(required_xcm_version) {
+						Ok(Some(new_data)) => Some((id, new_data)),
+						Ok(None) => None,
+						Err(_) => {
+							tracing::error!(
+								target: LOG_TARGET,
+								?id,
+								?required_xcm_version,
+								"`LockedFungibles` cannot be migrated!"
+							);
+							None
+						},
+					}
+				});
+			for (id, new_data) in locked_fungibles_to_migrate {
+				tracing::info!(
+					target: LOG_TARGET,
+					account_id = ?id,
+					?new_data,
+					"Migrating `LockedFungibles`"
+				);
+				LockedFungibles::<T>::insert(id, new_data);
+				weight.saturating_add(T::DbWeight::get().writes(1));
+			}
+
+			// check and migrate `RemoteLockedFungibles` - 1. step - just data
+			let remote_locked_fungibles_to_migrate =
+				RemoteLockedFungibles::<T>::iter().filter_map(|(id, data)| {
+					weight.saturating_add(T::DbWeight::get().reads(1));
+					match data.try_migrate(required_xcm_version) {
+						Ok(Some(new_data)) => Some((id, new_data)),
+						Ok(None) => None,
+						Err(_) => {
+							tracing::error!(
+								target: LOG_TARGET,
+								?id,
+								?required_xcm_version,
+								"`RemoteLockedFungibles` data cannot be migrated!"
+							);
+							None
+						},
+					}
+				});
+			for (id, new_data) in remote_locked_fungibles_to_migrate {
+				tracing::info!(
+					target: LOG_TARGET,
+					key = ?id,
+					amount = ?new_data.amount,
+					locker = ?new_data.locker,
+					owner = ?new_data.owner,
+					consumers_count = ?new_data.consumers.len(),
+					"Migrating `RemoteLockedFungibles` data"
+				);
+				RemoteLockedFungibles::<T>::insert(id, new_data);
+				weight.saturating_add(T::DbWeight::get().writes(1));
+			}
+
+			// check and migrate `RemoteLockedFungibles` - 2. step - key
+			let remote_locked_fungibles_keys_to_migrate = RemoteLockedFungibles::<T>::iter_keys()
+				.filter_map(|key| {
+					if key.needs_migration(required_xcm_version) {
+						let old_key = key.clone();
+						match key.try_migrate(required_xcm_version) {
+							Ok(Some(new_key)) => Some((old_key, new_key)),
+							Ok(None) => None,
+							Err(_) => {
+								tracing::error!(
+									target: LOG_TARGET,
+									id = ?old_key,
+									?required_xcm_version,
+									"`RemoteLockedFungibles` key cannot be migrated!"
+								);
+								None
+							},
+						}
+					} else {
+						None
+					}
+				});
+			for (old_key, new_key) in remote_locked_fungibles_keys_to_migrate {
+				weight.saturating_add(T::DbWeight::get().reads(1));
+				// make sure, that we don't override accidentally other data
+				if RemoteLockedFungibles::<T>::get(&new_key).is_some() {
+					tracing::error!(
+						target: LOG_TARGET,
+						?old_key,
+						?new_key,
+						"`RemoteLockedFungibles` already contains data for a `new_key`!"
+					);
+					// let's just skip for now, could be potentially caused with missing this
+					// migration before (manual clean-up?).
+					continue;
+				}
+
+				tracing::info!(
+					target: LOG_TARGET,
+					?old_key,
+					?new_key,
+					"Migrating `RemoteLockedFungibles` key"
+				);
+
+				// now we can swap the keys
+				RemoteLockedFungibles::<T>::swap::<
+					(
+						NMapKey<Twox64Concat, XcmVersion>,
+						NMapKey<Blake2_128Concat, T::AccountId>,
+						NMapKey<Blake2_128Concat, VersionedAssetId>,
+					),
+					_,
+					_,
+				>(&old_key, &new_key);
+				weight.saturating_add(T::DbWeight::get().writes(1));
+			}
+		}
+	}
+}
+
 pub mod v1 {
 	use super::*;
 	use crate::{CurrentMigration, VersionMigrationStage};
@@ -84,7 +386,80 @@ pub mod v1 {
 pub struct MigrateToLatestXcmVersion<T>(core::marker::PhantomData<T>);
 impl<T: Config> OnRuntimeUpgrade for MigrateToLatestXcmVersion<T> {
 	fn on_runtime_upgrade() -> Weight {
+		let mut weight = T::DbWeight::get().reads(1);
+
+		// trigger expensive/lazy migration (kind of multi-block)
 		CurrentMigration::<T>::put(VersionMigrationStage::default());
-		T::DbWeight::get().writes(1)
+		weight.saturating_accrue(T::DbWeight::get().writes(1));
+
+		// migrate other operational data to the latest XCM version in-place
+		let latest = CurrentXcmVersion::get();
+		Pallet::<T>::migrate_data_to_xcm_version(&mut weight, latest);
+
+		weight
+	}
+
+	#[cfg(feature = "try-runtime")]
+	fn post_upgrade(_: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
+		use data::NeedsMigration;
+		const LOG_TARGET: &str = "runtime::xcm::pallet_xcm::migrate_to_latest";
+
+		let latest = CurrentXcmVersion::get();
+
+		let number_of_queries_to_migrate = crate::Queries::<T>::iter()
+			.filter(|(id, data)| {
+				let needs_migration = data.needs_migration(latest);
+				if needs_migration {
+					tracing::warn!(
+						target: LOG_TARGET,
+						query_id = ?id,
+						query = ?data,
+						"Query was not migrated!"
+					)
+				}
+				needs_migration
+			})
+			.count();
+
+		let number_of_locked_fungibles_to_migrate = crate::LockedFungibles::<T>::iter()
+			.filter_map(|(id, data)| {
+				if data.needs_migration(latest) {
+					tracing::warn!(
+						target: LOG_TARGET,
+						?id,
+						?data,
+						"LockedFungibles item was not migrated!"
+					);
+					Some(true)
+				} else {
+					None
+				}
+			})
+			.count();
+
+		let number_of_remote_locked_fungibles_to_migrate =
+			crate::RemoteLockedFungibles::<T>::iter()
+				.filter_map(|(key, data)| {
+					if key.needs_migration(latest) || data.needs_migration(latest) {
+						tracing::warn!(
+							target: LOG_TARGET,
+							?key,
+							"RemoteLockedFungibles item was not migrated!"
+						);
+						Some(true)
+					} else {
+						None
+					}
+				})
+				.count();
+
+		ensure!(number_of_queries_to_migrate == 0, "must migrate all `Queries`.");
+		ensure!(number_of_locked_fungibles_to_migrate == 0, "must migrate all `LockedFungibles`.");
+		ensure!(
+			number_of_remote_locked_fungibles_to_migrate == 0,
+			"must migrate all `RemoteLockedFungibles`."
+		);
+
+		Ok(())
 	}
 }
diff --git a/polkadot/xcm/pallet-xcm/src/tests/mod.rs b/polkadot/xcm/pallet-xcm/src/tests/mod.rs
index c16c1a1ba986e5c95da3ce63dd58c52393408464..e98a8f8d2ce7fba605aab9dbfab6b4072a6e251a 100644
--- a/polkadot/xcm/pallet-xcm/src/tests/mod.rs
+++ b/polkadot/xcm/pallet-xcm/src/tests/mod.rs
@@ -19,11 +19,15 @@
 pub(crate) mod assets_transfer;
 
 use crate::{
-	mock::*, pallet::SupportedVersion, AssetTraps, Config, CurrentMigration, Error,
-	ExecuteControllerWeightInfo, LatestVersionedLocation, Pallet, Queries, QueryStatus,
-	RecordedXcm, ShouldRecordXcm, VersionDiscoveryQueue, VersionMigrationStage, VersionNotifiers,
+	migration::data::NeedsMigration,
+	mock::*,
+	pallet::{LockedFungibles, RemoteLockedFungibles, SupportedVersion},
+	AssetTraps, Config, CurrentMigration, Error, ExecuteControllerWeightInfo,
+	LatestVersionedLocation, Pallet, Queries, QueryStatus, RecordedXcm, RemoteLockedFungibleRecord,
+	ShouldRecordXcm, VersionDiscoveryQueue, VersionMigrationStage, VersionNotifiers,
 	VersionNotifyTargets, WeightInfo,
 };
+use bounded_collections::BoundedVec;
 use frame_support::{
 	assert_err_ignore_postinfo, assert_noop, assert_ok,
 	traits::{Currency, Hooks},
@@ -1258,6 +1262,168 @@ fn multistage_migration_works() {
 	})
 }
 
+#[test]
+fn migrate_data_to_xcm_version_works() {
+	new_test_ext_with_balances(vec![]).execute_with(|| {
+		// check `try-state`
+		assert!(Pallet::<Test>::do_try_state().is_ok());
+
+		let latest_version = XCM_VERSION;
+		let previous_version = XCM_VERSION - 1;
+
+		// `Queries` migration
+		{
+			let origin = VersionedLocation::from(Location::parent());
+			let query_id1 = 0;
+			let query_id2 = 2;
+			let query_as_latest =
+				QueryStatus::VersionNotifier { origin: origin.clone(), is_active: true };
+			let query_as_previous = QueryStatus::VersionNotifier {
+				origin: origin.into_version(previous_version).unwrap(),
+				is_active: true,
+			};
+			assert_ne!(query_as_latest, query_as_previous);
+			assert!(!query_as_latest.needs_migration(latest_version));
+			assert!(!query_as_latest.needs_migration(previous_version));
+			assert!(query_as_previous.needs_migration(latest_version));
+			assert!(!query_as_previous.needs_migration(previous_version));
+
+			// store two queries: migrated and not migrated
+			Queries::<Test>::insert(query_id1, query_as_latest.clone());
+			Queries::<Test>::insert(query_id2, query_as_previous);
+			assert!(Pallet::<Test>::do_try_state().is_ok());
+
+			// trigger migration
+			Pallet::<Test>::migrate_data_to_xcm_version(&mut Weight::zero(), latest_version);
+
+			// no change for query_id1
+			assert_eq!(Queries::<Test>::get(query_id1), Some(query_as_latest.clone()));
+			// change for query_id2
+			assert_eq!(Queries::<Test>::get(query_id2), Some(query_as_latest));
+			assert!(Pallet::<Test>::do_try_state().is_ok());
+		}
+
+		// `LockedFungibles` migration
+		{
+			let account1 = AccountId::new([13u8; 32]);
+			let account2 = AccountId::new([58u8; 32]);
+			let unlocker = VersionedLocation::from(Location::parent());
+			let lockeds_as_latest = BoundedVec::truncate_from(vec![(0, unlocker.clone())]);
+			let lockeds_as_previous = BoundedVec::truncate_from(vec![(
+				0,
+				unlocker.into_version(previous_version).unwrap(),
+			)]);
+			assert_ne!(lockeds_as_latest, lockeds_as_previous);
+			assert!(!lockeds_as_latest.needs_migration(latest_version));
+			assert!(!lockeds_as_latest.needs_migration(previous_version));
+			assert!(lockeds_as_previous.needs_migration(latest_version));
+			assert!(!lockeds_as_previous.needs_migration(previous_version));
+
+			// store two lockeds: migrated and not migrated
+			LockedFungibles::<Test>::insert(&account1, lockeds_as_latest.clone());
+			LockedFungibles::<Test>::insert(&account2, lockeds_as_previous);
+			assert!(Pallet::<Test>::do_try_state().is_ok());
+
+			// trigger migration
+			Pallet::<Test>::migrate_data_to_xcm_version(&mut Weight::zero(), latest_version);
+
+			// no change for account1
+			assert_eq!(LockedFungibles::<Test>::get(&account1), Some(lockeds_as_latest.clone()));
+			// change for account2
+			assert_eq!(LockedFungibles::<Test>::get(&account2), Some(lockeds_as_latest));
+			assert!(Pallet::<Test>::do_try_state().is_ok());
+		}
+
+		// `RemoteLockedFungibles` migration
+		{
+			let account1 = AccountId::new([13u8; 32]);
+			let account2 = AccountId::new([58u8; 32]);
+			let account3 = AccountId::new([97u8; 32]);
+			let asset_id = VersionedAssetId::from(AssetId(Location::parent()));
+			let owner = VersionedLocation::from(Location::parent());
+			let locker = VersionedLocation::from(Location::parent());
+			let key1_as_latest = (latest_version, account1, asset_id.clone());
+			let key2_as_latest = (latest_version, account2, asset_id.clone());
+			let key3_as_previous = (
+				previous_version,
+				account3.clone(),
+				asset_id.clone().into_version(previous_version).unwrap(),
+			);
+			let expected_key3_as_latest = (latest_version, account3, asset_id);
+			let data_as_latest = RemoteLockedFungibleRecord {
+				amount: Default::default(),
+				owner: owner.clone(),
+				locker: locker.clone(),
+				consumers: Default::default(),
+			};
+			let data_as_previous = RemoteLockedFungibleRecord {
+				amount: Default::default(),
+				owner: owner.into_version(previous_version).unwrap(),
+				locker: locker.into_version(previous_version).unwrap(),
+				consumers: Default::default(),
+			};
+			assert_ne!(data_as_latest.owner, data_as_previous.owner);
+			assert_ne!(data_as_latest.locker, data_as_previous.locker);
+			assert!(!key1_as_latest.needs_migration(latest_version));
+			assert!(!key1_as_latest.needs_migration(previous_version));
+			assert!(!key2_as_latest.needs_migration(latest_version));
+			assert!(!key2_as_latest.needs_migration(previous_version));
+			assert!(key3_as_previous.needs_migration(latest_version));
+			assert!(!key3_as_previous.needs_migration(previous_version));
+			assert!(!expected_key3_as_latest.needs_migration(latest_version));
+			assert!(!expected_key3_as_latest.needs_migration(previous_version));
+			assert!(!data_as_latest.needs_migration(latest_version));
+			assert!(!data_as_latest.needs_migration(previous_version));
+			assert!(data_as_previous.needs_migration(latest_version));
+			assert!(!data_as_previous.needs_migration(previous_version));
+
+			// store three lockeds:
+			// fully migrated
+			RemoteLockedFungibles::<Test>::insert(&key1_as_latest, data_as_latest.clone());
+			// only key migrated
+			RemoteLockedFungibles::<Test>::insert(&key2_as_latest, data_as_previous.clone());
+			// neither key nor data migrated
+			RemoteLockedFungibles::<Test>::insert(&key3_as_previous, data_as_previous);
+			assert!(Pallet::<Test>::do_try_state().is_ok());
+
+			// trigger migration
+			Pallet::<Test>::migrate_data_to_xcm_version(&mut Weight::zero(), latest_version);
+
+			let assert_locked_eq =
+				|left: Option<RemoteLockedFungibleRecord<_, _>>,
+				 right: Option<RemoteLockedFungibleRecord<_, _>>| {
+					match (left, right) {
+						(None, Some(_)) | (Some(_), None) =>
+							assert!(false, "Received unexpected message"),
+						(None, None) => (),
+						(Some(l), Some(r)) => {
+							assert_eq!(l.owner, r.owner);
+							assert_eq!(l.locker, r.locker);
+						},
+					}
+				};
+
+			// no change
+			assert_locked_eq(
+				RemoteLockedFungibles::<Test>::get(&key1_as_latest),
+				Some(data_as_latest.clone()),
+			);
+			// change - data migrated
+			assert_locked_eq(
+				RemoteLockedFungibles::<Test>::get(&key2_as_latest),
+				Some(data_as_latest.clone()),
+			);
+			// fully migrated
+			assert_locked_eq(RemoteLockedFungibles::<Test>::get(&key3_as_previous), None);
+			assert_locked_eq(
+				RemoteLockedFungibles::<Test>::get(&expected_key3_as_latest),
+				Some(data_as_latest.clone()),
+			);
+			assert!(Pallet::<Test>::do_try_state().is_ok());
+		}
+	})
+}
+
 #[test]
 fn record_xcm_works() {
 	let balances = vec![(ALICE, INITIAL_BALANCE)];
diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs
index 18bde3aab485a9660892ab1f5dc7c3de4b508338..aebdee69c5eca99105c289b09c122b5f76aec060 100644
--- a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs
+++ b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs
@@ -117,7 +117,6 @@ parameter_types! {
 	pub const AnyNetwork: Option<NetworkId> = None;
 	pub UniversalLocation: InteriorLocation = (ByGenesis([0; 32]), Parachain(42)).into();
 	pub UnitWeightCost: u64 = 1_000;
-	pub static AdvertisedXcmVersion: u32 = 3;
 	pub const BaseXcmWeight: Weight = Weight::from_parts(1_000, 1_000);
 	pub CurrencyPerSecondPerByte: (AssetId, u128, u128) = (AssetId(RelayLocation::get()), 1, 1);
 	pub TrustedAssets: (AssetFilter, Location) = (All.into(), Here.into());
@@ -258,7 +257,7 @@ impl pallet_xcm::Config for Test {
 	type RuntimeOrigin = RuntimeOrigin;
 	type RuntimeCall = RuntimeCall;
 	const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100;
-	type AdvertisedXcmVersion = AdvertisedXcmVersion;
+	type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion;
 	type TrustedLockers = ();
 	type SovereignAccountOf = SovereignAccountOf;
 	type Currency = Balances;
diff --git a/polkadot/xcm/xcm-runtime-apis/tests/mock.rs b/polkadot/xcm/xcm-runtime-apis/tests/mock.rs
index c76b26fcd2a337545450da4d77cb0103c3e8eb8c..b2a416468e7cd97794594f9485b4e429292ec255 100644
--- a/polkadot/xcm/xcm-runtime-apis/tests/mock.rs
+++ b/polkadot/xcm/xcm-runtime-apis/tests/mock.rs
@@ -137,7 +137,6 @@ parameter_types! {
 	pub const MaxInstructions: u32 = 100;
 	pub const NativeTokenPerSecondPerByte: (AssetId, u128, u128) = (AssetId(HereLocation::get()), 1, 1);
 	pub UniversalLocation: InteriorLocation = [GlobalConsensus(NetworkId::Westend), Parachain(2000)].into();
-	pub static AdvertisedXcmVersion: XcmVersion = 4;
 	pub const HereLocation: Location = Location::here();
 	pub const RelayLocation: Location = Location::parent();
 	pub const MaxAssetsIntoHolding: u32 = 64;
@@ -341,7 +340,7 @@ impl pallet_xcm::Config for TestRuntime {
 	type RuntimeOrigin = RuntimeOrigin;
 	type RuntimeCall = RuntimeCall;
 	const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100;
-	type AdvertisedXcmVersion = AdvertisedXcmVersion;
+	type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion;
 	type AdminOrigin = EnsureRoot<AccountId>;
 	type TrustedLockers = ();
 	type SovereignAccountOf = ();
diff --git a/prdoc/pr_6148.prdoc b/prdoc/pr_6148.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..430a58dfefbb509b3b1b9242dbff6017c1227349
--- /dev/null
+++ b/prdoc/pr_6148.prdoc
@@ -0,0 +1,17 @@
+title: Fix migrations for pallet-xcm
+doc:
+- audience: Runtime Dev
+  description: |-
+    `pallet-xcm` stores some operational data that uses `Versioned*` XCM types. When we add a new XCM version (XV), we deprecate XV-2 and remove XV-3.
+    This PR extends the existing `MigrateToLatestXcmVersion` to include migration for the `Queries`, `LockedFungibles`, and `RemoteLockedFungibles` storage types.
+    Additionally, more checks were added to `try_state` for these types.
+
+crates:
+- name: westend-runtime
+  bump: patch
+- name: staging-xcm-builder
+  bump: none
+- name: xcm-runtime-apis
+  bump: none
+- name: pallet-xcm
+  bump: patch