referrerpolicy=no-referrer-when-downgrade

pallet_assets_precompiles/
migration.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Migrations for `pallet-assets-precompiles`.
19
20use crate::{foreign_assets::pallet, weights::WeightInfo};
21use core::marker::PhantomData;
22use frame_support::{
23	defensive,
24	migrations::{MigrationId, SteppedMigration, SteppedMigrationError},
25	weights::WeightMeter,
26};
27
28const PRECOMPILE_MAPPINGS_MIGRATION_ID: &[u8; 32] = b"foreign-asset-precompile-mapping";
29const LOG_TARGET: &str = "runtime::MigrateForeignAssetPrecompileMappings";
30
31/// Migration to backfill foreign asset precompile mappings for existing assets.
32///
33/// This migration populates the bidirectional mapping between foreign asset IDs (e.g., XCM
34/// Locations) and sequential u32 indices in `pallet_assets_precompiles` for all existing foreign
35/// assets.
36///
37/// The mapping enables EVM precompile addresses for foreign assets, where the u32 index
38/// is embedded in the first 4 bytes of the 20-byte Ethereum address.
39///
40/// # Background
41///
42/// Foreign assets are identified by types (like XCM Location) that do not fit in 4 bytes.
43/// In order to facilitate EVM precompile addresses for these assets, a mapping is maintained
44/// between a sequential u32 index and the actual foreign asset ID.
45/// The pallet maintains a bidirectional mapping:
46/// - `AssetIndexToForeignAssetId`: u32 index -> Foreign Asset ID
47/// - `ForeignAssetIdToAssetIndex`: Foreign Asset ID -> u32 index
48///
49/// While new foreign assets automatically get mapped via the `AssetsCallback` hook,
50/// this migration ensures existing foreign assets (created before the mapping was introduced)
51/// are also added to the mapping with sequential indices.
52///
53/// # Type Parameters
54///
55/// - `T`: The runtime configuration implementing both `pallet_assets::Config<I>` and
56///   `pallet::Config`
57/// - `I`: The pallet_assets instance identifier (e.g., `ForeignAssetsInstance`)
58/// - `W`: The weight info implementation for the migration benchmarks
59///
60/// # Usage in Runtime
61///
62/// Add this to the runtime's `Migrations` tuple in lib.rs:
63///
64/// ```ignore
65/// pub type Migrations = (
66///     // ... other migrations ...
67///     pallet_assets_precompiles::MigrateForeignAssetPrecompileMappings<Runtime, ForeignAssetsInstance, ()>,
68/// );
69/// ```
70///
71/// # Safety
72///
73/// - Non-destructive: Does not modify any asset data, only adds mappings
74/// - Sequential indices: Each migrated asset gets the next available index
75///
76/// NB: If the SCALE encoding of `AssetId` (e.g. `xcm::v5::Location`) ever changes,
77/// the keys in `ForeignAssetIdToAssetIndex` must be migrated **in-place** (decode old
78/// encoding → re-encode with new encoding → reinsert with the same index). Simply
79/// clearing and repopulating would reassign indices, breaking EVM contracts that
80/// cached precompile addresses derived from those indices.
81
82pub struct MigrateForeignAssetPrecompileMappings<T, I, W>(PhantomData<(T, I, W)>);
83
84impl<T, I, W> SteppedMigration for MigrateForeignAssetPrecompileMappings<T, I, W>
85where
86	T: pallet_assets::Config<I>
87		+ pallet::Config<ForeignAssetId = <T as pallet_assets::Config<I>>::AssetId>,
88	I: 'static,
89	W: WeightInfo,
90{
91	type Cursor = <T as pallet_assets::Config<I>>::AssetId;
92	type Identifier = MigrationId<32>;
93
94	fn id() -> Self::Identifier {
95		MigrationId { pallet_id: *PRECOMPILE_MAPPINGS_MIGRATION_ID, version_from: 0, version_to: 1 }
96	}
97
98	fn step(
99		cursor: Option<Self::Cursor>,
100		meter: &mut WeightMeter,
101	) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
102		let required = W::migrate_foreign_asset_step();
103		if meter.remaining().any_lt(required) {
104			return Err(SteppedMigrationError::InsufficientWeight { required });
105		}
106
107		let mut last_key = cursor;
108
109		loop {
110			if meter.try_consume(required).is_err() {
111				break;
112			}
113
114			let Some(asset_id) = Self::peek_next_asset(last_key.as_ref()) else {
115				log::info!(target: LOG_TARGET, "migration finished");
116				return Ok(None);
117			};
118
119			if pallet::Pallet::<T>::asset_index_of(&asset_id).is_none() {
120				match pallet::Pallet::<T>::insert_asset_mapping(&asset_id) {
121					Ok(asset_index) => log::debug!(
122						target: LOG_TARGET,
123						"Migrated asset {:?} to index {:?}",
124						asset_id,
125						asset_index
126					),
127					Err(()) => {
128						// qed: we already checked that the index does *not* exist
129						defensive!("insert_asset_mapping failed during migration; this should be unreachable unless NextAssetIndex overflowed u32::MAX");
130					},
131				}
132			} else {
133				log::debug!(
134					target: LOG_TARGET,
135					"Skipping already-mapped asset {:?}",
136					asset_id
137				);
138			}
139			last_key = Some(asset_id);
140		}
141
142		Ok(last_key)
143	}
144
145	#[cfg(feature = "try-runtime")]
146	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
147		use codec::Encode;
148
149		let mut unmapped_assets = alloc::vec::Vec::new();
150
151		for (asset_id, _) in pallet_assets::Asset::<T, I>::iter() {
152			if pallet::Pallet::<T>::asset_index_of(&asset_id).is_none() {
153				unmapped_assets.push(asset_id);
154			}
155		}
156
157		log::info!(
158			target: LOG_TARGET,
159			"Found {} foreign assets needing migration",
160			unmapped_assets.len()
161		);
162
163		Ok(unmapped_assets.encode())
164	}
165
166	#[cfg(feature = "try-runtime")]
167	fn post_upgrade(state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
168		use codec::Decode;
169
170		let unmapped_assets: alloc::vec::Vec<<T as pallet_assets::Config<I>>::AssetId> =
171			Decode::decode(&mut &state[..])
172				.map_err(|_| sp_runtime::TryRuntimeError::Other("Failed to decode state"))?;
173
174		let mut migrated = 0u64;
175
176		for asset_id in &unmapped_assets {
177			// Check that a mapping now exists for this asset
178			match pallet::Pallet::<T>::asset_index_of(asset_id) {
179				Some(index) => {
180					// Verify reverse mapping is consistent
181					match pallet::Pallet::<T>::asset_id_of(index) {
182						Some(stored_id) if stored_id == *asset_id => {
183							migrated = migrated.saturating_add(1);
184						},
185						_ => {
186							return Err(sp_runtime::TryRuntimeError::Other(
187								"Reverse mapping mismatch",
188							))
189						},
190					}
191				},
192				None => {
193					log::error!(
194						target: LOG_TARGET,
195						"Asset {:?} not migrated",
196						asset_id
197					);
198					return Err(sp_runtime::TryRuntimeError::Other("Asset not migrated"));
199				},
200			}
201		}
202
203		log::info!(
204			target: LOG_TARGET,
205			"Verified {} foreign asset mappings",
206			migrated
207		);
208
209		Ok(())
210	}
211}
212
213impl<T, I, W> MigrateForeignAssetPrecompileMappings<T, I, W>
214where
215	T: pallet_assets::Config<I>
216		+ pallet::Config<ForeignAssetId = <T as pallet_assets::Config<I>>::AssetId>,
217	I: 'static,
218	W: WeightInfo,
219{
220	/// Peeks at the next asset without performing any migration.
221	/// Used to determine whether there is another asset to process.
222	fn peek_next_asset(
223		maybe_last_key: Option<&<T as pallet_assets::Config<I>>::AssetId>,
224	) -> Option<<T as pallet_assets::Config<I>>::AssetId> {
225		let mut iter = if let Some(last_key) = maybe_last_key {
226			pallet_assets::Asset::<T, I>::iter_keys_from(
227				pallet_assets::Asset::<T, I>::hashed_key_for(last_key),
228			)
229		} else {
230			pallet_assets::Asset::<T, I>::iter_keys()
231		};
232
233		iter.next()
234	}
235}