pallet_psm/migrations/
decimals.rs1#[cfg(feature = "try-runtime")]
47use alloc::vec::Vec;
48use frame_support::{
49 migrations::VersionedMigration,
50 pallet_prelude::Weight,
51 traits::{
52 fungible::metadata::Inspect as FungibleMetadataInspect,
53 fungibles::metadata::Inspect as FungiblesMetadataInspect, Get, UncheckedOnRuntimeUpgrade,
54 },
55};
56
57use crate::{
58 pallet::{
59 CircuitBreakerLevel, ExternalAssets, ExternalDecimals, InternalDecimals, MAX_DECIMALS_DIFF,
60 },
61 Config, Pallet,
62};
63
64#[cfg(feature = "try-runtime")]
65use frame_support::ensure;
66#[cfg(feature = "try-runtime")]
67use sp_runtime::TryRuntimeError;
68
69const LOG_TARGET: &str = "runtime::psm::migration::populate_decimals";
70
71pub type PopulateDecimals<T> = VersionedMigration<
75 1,
76 2,
77 InnerPopulateDecimals<T>,
78 Pallet<T>,
79 <T as frame_system::Config>::DbWeight,
80>;
81
82pub struct InnerPopulateDecimals<T>(core::marker::PhantomData<T>);
88
89impl<T: Config> UncheckedOnRuntimeUpgrade for InnerPopulateDecimals<T> {
90 fn on_runtime_upgrade() -> Weight {
91 log::info!(
92 target: LOG_TARGET,
93 "Running PopulateDecimals: backfilling decimal snapshots"
94 );
95
96 let mut reads = 0u64;
97 let mut writes = 0u64;
98
99 reads += 2;
101 let internal_decimals = T::InternalAsset::decimals();
102 if !InternalDecimals::<T>::exists() {
103 InternalDecimals::<T>::put(internal_decimals);
104 writes += 1;
105 }
106
107 for (asset_id, status) in ExternalAssets::<T>::iter() {
109 reads += 3; if ExternalDecimals::<T>::contains_key(&asset_id) {
111 log::info!(
112 target: LOG_TARGET,
113 "Asset {:?} already has a decimals snapshot, skipping",
114 asset_id,
115 );
116 continue;
117 }
118
119 let asset_decimals = T::Fungibles::decimals(asset_id.clone());
120 ExternalDecimals::<T>::insert(&asset_id, asset_decimals);
121 writes += 1;
122
123 let diff = asset_decimals.abs_diff(internal_decimals) as u32;
124 if diff > MAX_DECIMALS_DIFF {
125 if status != CircuitBreakerLevel::AllDisabled {
130 ExternalAssets::<T>::insert(&asset_id, CircuitBreakerLevel::AllDisabled);
131 writes += 1;
132 }
133 log::warn!(
134 target: LOG_TARGET,
135 "Asset {:?} decimals diff ({}) exceeds MAX_DECIMALS_DIFF ({}); disabling",
136 asset_id,
137 diff,
138 MAX_DECIMALS_DIFF,
139 );
140 } else {
141 log::info!(
142 target: LOG_TARGET,
143 "Populated decimals snapshot for asset {:?} (decimals={})",
144 asset_id,
145 asset_decimals,
146 );
147 }
148 }
149
150 log::info!(
151 target: LOG_TARGET,
152 "PopulateDecimals complete"
153 );
154
155 T::DbWeight::get().reads_writes(reads, writes)
156 }
157
158 #[cfg(feature = "try-runtime")]
159 fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
160 Ok(Vec::new())
161 }
162
163 #[cfg(feature = "try-runtime")]
164 fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
165 ensure!(
167 InternalDecimals::<T>::get() == Some(T::InternalAsset::decimals()),
168 "InternalDecimals snapshot missing or stale after migration"
169 );
170
171 let internal_decimals = T::InternalAsset::decimals();
172 for (asset_id, status) in ExternalAssets::<T>::iter() {
173 let snapshot = ExternalDecimals::<T>::get(&asset_id)
174 .ok_or("Approved external asset missing decimals snapshot after migration")?;
175 ensure!(
176 snapshot == T::Fungibles::decimals(asset_id),
177 "ExternalDecimals snapshot differs from live metadata after migration"
178 );
179 let diff = snapshot.abs_diff(internal_decimals) as u32;
180 if diff > MAX_DECIMALS_DIFF {
181 ensure!(
182 status == CircuitBreakerLevel::AllDisabled,
183 "Out-of-range external asset was not disabled by migration"
184 );
185 }
186 }
187
188 Ok(())
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::{
196 mock::{
197 new_test_ext, RuntimeOrigin, Test, ALICE, DAI_MOCK_ASSET_ID, USDC_ASSET_ID,
198 USDT_ASSET_ID,
199 },
200 Pallet,
201 };
202 use frame_support::{
203 assert_ok,
204 traits::{GetStorageVersion, OnRuntimeUpgrade, StorageVersion},
205 };
206
207 fn prepare_v1() {
210 StorageVersion::new(1).put::<Pallet<Test>>();
211 }
212
213 #[test]
214 fn populate_decimals_backfills_existing_assets() {
215 new_test_ext().execute_with(|| {
216 prepare_v1();
219 InternalDecimals::<Test>::kill();
220 ExternalDecimals::<Test>::remove(USDC_ASSET_ID);
221 ExternalDecimals::<Test>::remove(USDT_ASSET_ID);
222
223 PopulateDecimals::<Test>::on_runtime_upgrade();
224
225 assert_eq!(InternalDecimals::<Test>::get(), Some(6));
226 assert_eq!(ExternalDecimals::<Test>::get(USDC_ASSET_ID), Some(6));
227 assert_eq!(ExternalDecimals::<Test>::get(USDT_ASSET_ID), Some(6));
228 assert_eq!(
230 ExternalAssets::<Test>::get(USDC_ASSET_ID),
231 Some(CircuitBreakerLevel::AllEnabled)
232 );
233 assert_eq!(
234 ExternalAssets::<Test>::get(USDT_ASSET_ID),
235 Some(CircuitBreakerLevel::AllEnabled)
236 );
237 });
238 }
239
240 #[test]
241 fn populate_decimals_does_not_overwrite_existing_snapshots() {
242 new_test_ext().execute_with(|| {
243 prepare_v1();
244 ExternalDecimals::<Test>::insert(USDC_ASSET_ID, 42u8);
247
248 PopulateDecimals::<Test>::on_runtime_upgrade();
249
250 assert_eq!(ExternalDecimals::<Test>::get(USDC_ASSET_ID), Some(42));
251 });
252 }
253
254 #[test]
255 fn populate_decimals_disables_out_of_range_assets() {
256 new_test_ext().execute_with(|| {
257 use crate::mock::{Assets, INTERNAL_ASSET_ID};
263 use frame_support::traits::fungibles::metadata::Mutate as MetadataMutate;
264
265 assert_ok!(Pallet::<Test>::add_external_asset(
266 RuntimeOrigin::root(),
267 DAI_MOCK_ASSET_ID
268 ));
269 assert_ok!(<Assets as MetadataMutate<u64>>::set(
274 INTERNAL_ASSET_ID,
275 &ALICE,
276 b"Internal Asset".to_vec(),
277 b"INTERNAL".to_vec(),
278 45,
279 ));
280
281 ExternalDecimals::<Test>::remove(DAI_MOCK_ASSET_ID);
284 InternalDecimals::<Test>::kill();
285 prepare_v1();
286
287 PopulateDecimals::<Test>::on_runtime_upgrade();
288
289 assert_eq!(InternalDecimals::<Test>::get(), Some(45));
291 assert_eq!(ExternalDecimals::<Test>::get(DAI_MOCK_ASSET_ID), Some(18));
292 assert_eq!(
294 ExternalAssets::<Test>::get(DAI_MOCK_ASSET_ID),
295 Some(CircuitBreakerLevel::AllDisabled)
296 );
297 assert_eq!(
299 ExternalAssets::<Test>::get(USDC_ASSET_ID),
300 Some(CircuitBreakerLevel::AllEnabled)
301 );
302 });
303 }
304
305 #[test]
306 fn populate_decimals_runs_once_then_skips() {
307 new_test_ext().execute_with(|| {
308 prepare_v1();
309 InternalDecimals::<Test>::kill();
310 ExternalDecimals::<Test>::remove(USDC_ASSET_ID);
311
312 PopulateDecimals::<Test>::on_runtime_upgrade();
314 assert_eq!(Pallet::<Test>::on_chain_storage_version(), StorageVersion::new(2));
315 let stable1 = InternalDecimals::<Test>::get();
316 let usdc1 = ExternalDecimals::<Test>::get(USDC_ASSET_ID);
317
318 PopulateDecimals::<Test>::on_runtime_upgrade();
321 assert_eq!(InternalDecimals::<Test>::get(), stable1);
322 assert_eq!(ExternalDecimals::<Test>::get(USDC_ASSET_ID), usdc1);
323 assert_eq!(Pallet::<Test>::on_chain_storage_version(), StorageVersion::new(2));
324 });
325 }
326
327 #[test]
328 fn populate_decimals_skips_when_not_on_version_one() {
329 new_test_ext().execute_with(|| {
330 StorageVersion::new(2).put::<Pallet<Test>>();
332
333 ExternalDecimals::<Test>::remove(USDC_ASSET_ID);
334 PopulateDecimals::<Test>::on_runtime_upgrade();
335
336 assert_eq!(ExternalDecimals::<Test>::get(USDC_ASSET_ID), None);
338 assert_eq!(Pallet::<Test>::on_chain_storage_version(), StorageVersion::new(2));
340 });
341 }
342}