From eaf41f6c9ea6cd5799cf6b36dbca9b6466e55b39 Mon Sep 17 00:00:00 2001 From: Ahmad Kaouk Date: Thu, 14 Nov 2024 15:03:41 +0100 Subject: [PATCH] add test for start_foreign_asset_migration call --- Cargo.lock | 1 + pallets/moonbeam-lazy-migrations/Cargo.toml | 2 + .../src/foreign_asset.rs | 79 +++--- pallets/moonbeam-lazy-migrations/src/mock.rs | 186 ++++++++++++--- pallets/moonbeam-lazy-migrations/src/tests.rs | 224 ++++++++++++++++-- 5 files changed, 405 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 823836d671..64a06b8e0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9554,6 +9554,7 @@ dependencies = [ "pallet-scheduler", "pallet-timestamp", "parity-scale-codec", + "precompile-utils", "rlp", "scale-info", "sp-core", diff --git a/pallets/moonbeam-lazy-migrations/Cargo.toml b/pallets/moonbeam-lazy-migrations/Cargo.toml index 3a8b58bc04..233a5a0168 100644 --- a/pallets/moonbeam-lazy-migrations/Cargo.toml +++ b/pallets/moonbeam-lazy-migrations/Cargo.toml @@ -38,6 +38,7 @@ frame-benchmarking = { workspace = true, optional = true } frame-benchmarking = { workspace = true, features = ["std"] } pallet-balances = { workspace = true, features = ["std", "insecure_zero_ed"] } pallet-timestamp = { workspace = true, features = ["std"] } +precompile-utils = { workspace = true, features = ["std"] } rlp = { workspace = true, features = ["std"] } sp-io = { workspace = true, features = ["std"] } @@ -58,6 +59,7 @@ std = [ "pallet-assets/std", "pallet-moonbeam-foreign-assets/std", "pallet-asset-manager/std", + "precompile-utils/std", "cumulus-primitives-storage-weight-reclaim/std", "rlp/std", "xcm-primitives/std", diff --git a/pallets/moonbeam-lazy-migrations/src/foreign_asset.rs b/pallets/moonbeam-lazy-migrations/src/foreign_asset.rs index 209838801e..ee8f50bc52 100644 --- a/pallets/moonbeam-lazy-migrations/src/foreign_asset.rs +++ b/pallets/moonbeam-lazy-migrations/src/foreign_asset.rs @@ -39,9 +39,9 @@ impl Default for ForeignAssetMigrationStatus { #[derive(Encode, Decode, scale_info::TypeInfo, PartialEq, MaxEncodedLen)] pub(super) struct ForeignAssetMigreationInfo { - asset_id: u128, - remaining_balances: u32, - remaining_approvals: u32, + pub(super) asset_id: u128, + pub(super) remaining_balances: u32, + pub(super) remaining_approvals: u32, } impl Pallet @@ -61,44 +61,43 @@ where Error::::MigrationNotFinished ); - let asset = - pallet_assets::Asset::::get(asset_id).ok_or(Error::::AssetNotFound)?; - // Freeze the asset - pallet_assets::Pallet::::freeze_asset(origin.clone(), asset_id.into())?; - - let decimals = pallet_assets::Pallet::::decimals(asset_id); - - let symbol = pallet_assets::Pallet::::symbol(asset_id) - .try_into() - .map_err(|_| Error::::SymbolTooLong)?; - - let name = as Inspect<_>>::name(asset_id) - .try_into() - .map_err(|_| Error::::NameTooLong)?; - - let xcm_location: Location = - pallet_asset_manager::Pallet::::get_asset_type(asset_id) - .ok_or(Error::::AssetTypeNotFound)? - .into() - .ok_or(Error::::LocationNotFound)?; - - // Create the SC for the asset with moonbeam foreign assets pallet - pallet_moonbeam_foreign_assets::Pallet::::create_foreign_asset( - origin, - asset_id, - xcm_location, - decimals, - symbol, - name, - )?; - - *status = ForeignAssetMigrationStatus::Migrating(ForeignAssetMigreationInfo { - asset_id, - remaining_balances: asset.accounts, - remaining_approvals: asset.approvals, - }); - Ok(()) + pallet_assets::Asset::::try_mutate_exists(asset_id, |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::AssetNotFound)?; + + details.status = pallet_assets::AssetStatus::Frozen; + + let decimals = pallet_assets::Pallet::::decimals(asset_id); + let symbol = pallet_assets::Pallet::::symbol(asset_id) + .try_into() + .map_err(|_| Error::::SymbolTooLong)?; + let name = as Inspect<_>>::name(asset_id) + .try_into() + .map_err(|_| Error::::NameTooLong)?; + let xcm_location: Location = + pallet_asset_manager::Pallet::::get_asset_type(asset_id) + .ok_or(Error::::AssetTypeNotFound)? + .into() + .ok_or(Error::::LocationNotFound)?; + + // Create the SC for the asset with moonbeam foreign assets pallet + pallet_moonbeam_foreign_assets::Pallet::::create_foreign_asset( + origin, + asset_id, + xcm_location, + decimals, + symbol, + name, + )?; + + *status = ForeignAssetMigrationStatus::Migrating(ForeignAssetMigreationInfo { + asset_id, + remaining_balances: details.accounts, + remaining_approvals: details.approvals, + }); + + Ok(()) + }) }) } diff --git a/pallets/moonbeam-lazy-migrations/src/mock.rs b/pallets/moonbeam-lazy-migrations/src/mock.rs index 8d9b074408..353bc38a25 100644 --- a/pallets/moonbeam-lazy-migrations/src/mock.rs +++ b/pallets/moonbeam-lazy-migrations/src/mock.rs @@ -18,20 +18,25 @@ use super::*; use crate as pallet_moonbeam_lazy_migrations; +use frame_support::traits::AsEnsureOriginWithArg; use frame_support::{ construct_runtime, ord_parameter_types, parameter_types, traits::{EqualPrivilegeOnly, Everything, SortedMembers}, weights::{RuntimeDbWeight, Weight}, }; -use frame_system::EnsureRoot; -use pallet_evm::{AddressMapping, EnsureAddressTruncated}; +use frame_system::{EnsureRoot, EnsureSigned}; +use pallet_asset_manager::AssetRegistrar; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot}; +use precompile_utils::testing::MockAccount; use sp_core::{ConstU32, H160, H256, U256}; use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, - AccountId32, BuildStorage, Perbill, + traits::{BlakeTwo256, Hash, IdentityLookup}, + BuildStorage, Perbill, }; +pub type AssetId = u128; pub type Balance = u128; +pub type AccountId = MockAccount; type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -44,6 +49,9 @@ construct_runtime!( EVM: pallet_evm, LazyMigrations: pallet_moonbeam_lazy_migrations::{Pallet, Call}, Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event}, + Assets: pallet_assets::{Pallet, Call, Storage, Event}, + AssetManager: pallet_asset_manager::{Pallet, Call, Storage, Event}, + MoonbeamForeignAssets: pallet_moonbeam_foreign_assets::{Pallet, Call, Storage, Event}, } ); @@ -72,7 +80,7 @@ impl frame_system::Config for Test { type RuntimeCall = RuntimeCall; type Hash = H256; type Hashing = BlakeTwo256; - type AccountId = AccountId32; + type AccountId = AccountId; type Lookup = IdentityLookup; type RuntimeEvent = RuntimeEvent; type BlockHashCount = BlockHashCount; @@ -154,40 +162,21 @@ impl pallet_timestamp::Config for Test { type WeightInfo = (); } -const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; -/// Block Storage Limit in bytes. Set to 40KB. -const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024; - parameter_types! { pub BlockGasLimit: U256 = U256::from(u64::MAX); - pub WeightPerGas: Weight = Weight::from_parts(1, 0); - pub GasLimitPovSizeRatio: u64 = { - let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); - block_gas_limit.saturating_div(MAX_POV_SIZE) - }; - pub GasLimitStorageGrowthRatio: u64 = { - let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); - block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT) - }; + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = 16; + pub GasLimitStorageGrowthRatio: u64 = 366; pub SuicideQuickClearLimit: u32 = 0; } -pub struct HashedAddressMapping; - -impl AddressMapping for HashedAddressMapping { - fn into_account_id(address: H160) -> AccountId32 { - let mut data = [0u8; 32]; - data[0..20].copy_from_slice(&address[..]); - AccountId32::from(Into::<[u8; 32]>::into(data)) - } -} impl pallet_evm::Config for Test { type FeeCalculator = (); type GasWeightMapping = pallet_evm::FixedGasWeightMapping; type WeightPerGas = WeightPerGas; - type CallOrigin = EnsureAddressTruncated; - type WithdrawOrigin = EnsureAddressTruncated; - type AddressMapping = HashedAddressMapping; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; type Currency = Balances; type RuntimeEvent = RuntimeEvent; type Runner = pallet_evm::runner::stack::Runner; @@ -202,23 +191,154 @@ impl pallet_evm::Config for Test { type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; - type WeightInfo = pallet_evm::weights::SubstrateWeight; + type WeightInfo = (); type SuicideQuickClearLimit = SuicideQuickClearLimit; } +parameter_types! { + pub const AssetDeposit: u128 = 1; + pub const MetadataDepositBase: u128 = 1; + pub const MetadataDepositPerByte: u128 = 1; + pub const ApprovalDeposit: u128 = 1; + pub const AssetsStringLimit: u32 = 50; + pub const AssetAccountDeposit: u128 = 1; +} + +impl pallet_assets::Config<()> for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetId; + type Currency = Balances; + type ForceOrigin = EnsureRoot; + type AssetDeposit = AssetDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = AssetsStringLimit; + type Freezer = (); + type Extra = (); + type AssetAccountDeposit = AssetAccountDeposit; + type WeightInfo = (); + type RemoveItemsLimit = ConstU32<656>; + type AssetIdParameter = AssetId; + type CreateOrigin = AsEnsureOriginWithArg>; + type CallbackHandle = (); +} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum MockAssetType { + Xcm(Location), + MockAsset(AssetId), +} + +impl Default for MockAssetType { + fn default() -> Self { + Self::MockAsset(0) + } +} + +impl From for AssetId { + fn from(asset: MockAssetType) -> AssetId { + match asset { + MockAssetType::MockAsset(id) => id, + MockAssetType::Xcm(id) => { + let mut result: [u8; 16] = [0u8; 16]; + let hash: H256 = id.using_encoded(::Hashing::hash); + result.copy_from_slice(&hash.as_fixed_bytes()[0..16]); + u128::from_le_bytes(result) + } + } + } +} + +impl From for MockAssetType { + fn from(location: Location) -> Self { + Self::Xcm(location) + } +} + +impl Into> for MockAssetType { + fn into(self) -> Option { + match self { + Self::Xcm(location) => Some(location), + _ => None, + } + } +} + +pub struct MockAssetPalletRegistrar; + +impl AssetRegistrar for MockAssetPalletRegistrar { + fn create_foreign_asset( + _asset: u128, + _min_balance: u128, + _metadata: u32, + _is_sufficient: bool, + ) -> Result<(), DispatchError> { + Ok(()) + } + + fn destroy_foreign_asset(_asset: u128) -> Result<(), DispatchError> { + Ok(()) + } + + fn destroy_asset_dispatch_info_weight(_asset: u128) -> Weight { + Weight::from_parts(0, 0) + } +} + +impl pallet_asset_manager::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetId; + type AssetRegistrarMetadata = u32; + type ForeignAssetType = MockAssetType; + type AssetRegistrar = MockAssetPalletRegistrar; + type ForeignAssetModifierOrigin = EnsureRoot; + type WeightInfo = (); +} + +pub struct AccountIdToH160; +impl sp_runtime::traits::Convert for AccountIdToH160 { + fn convert(account_id: AccountId) -> H160 { + account_id.into() + } +} + +impl pallet_moonbeam_foreign_assets::Config for Test { + type AccountIdToH160 = AccountIdToH160; + type AssetIdFilter = Everything; + type EvmRunner = pallet_evm::runner::stack::Runner; + type ForeignAssetCreatorOrigin = EnsureRoot; + type ForeignAssetFreezerOrigin = EnsureRoot; + type ForeignAssetModifierOrigin = EnsureRoot; + type ForeignAssetUnfreezerOrigin = EnsureRoot; + type OnForeignAssetCreated = (); + type MaxForeignAssets = ConstU32<3>; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type XcmLocationToH160 = (); +} + impl Config for Test { type WeightInfo = (); + type ForeignAssetMigratorOrigin = EnsureRoot; } /// Externality builder for pallet migration's mock runtime pub(crate) struct ExtBuilder { // endowed accounts with balances - balances: Vec<(AccountId32, Balance)>, + balances: Vec<(AccountId, Balance)>, } impl Default for ExtBuilder { fn default() -> ExtBuilder { - ExtBuilder { balances: vec![] } + ExtBuilder { + balances: vec![ + (AccountId::from([1; 20]), 1000), + (AccountId::from([2; 20]), 1000), + ], + } } } diff --git a/pallets/moonbeam-lazy-migrations/src/tests.rs b/pallets/moonbeam-lazy-migrations/src/tests.rs index ff460d1fa1..7666a4d082 100644 --- a/pallets/moonbeam-lazy-migrations/src/tests.rs +++ b/pallets/moonbeam-lazy-migrations/src/tests.rs @@ -15,9 +15,16 @@ // along with Moonbeam. If not, see . //! Unit testing +use crate::{foreign_asset::ForeignAssetMigrationStatus, mock::AssetId}; +use pallet_evm::AddressMapping; +use sp_runtime::DispatchError; +use xcm::latest::Location; use { crate::{ - mock::{ExtBuilder, LazyMigrations, RuntimeOrigin, Test}, + mock::{ + AccountId, AssetManager, Assets, ExtBuilder, LazyMigrations, MockAssetType, + RuntimeOrigin, Test, + }, Error, StateMigrationStatus, StateMigrationStatusValue, MAX_ITEM_PROOF_SIZE, PROOF_SIZE_BUFFER, }, @@ -25,11 +32,9 @@ use { rlp::RlpStream, sp_core::{H160, H256}, sp_io::hashing::keccak_256, - sp_runtime::{traits::Bounded, AccountId32}, + sp_runtime::traits::Bounded, }; -use pallet_evm::AddressMapping; - // Helper function that calculates the contract address pub fn contract_address(sender: H160, nonce: u64) -> H160 { let mut rlp = RlpStream::new_list(2); @@ -88,7 +93,7 @@ fn test_clear_suicided_contract_succesfull() { // Call the extrinsic to delete the storage entries let _ = LazyMigrations::clear_suicided_storage( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), vec![contract_address].try_into().unwrap(), 1000, ); @@ -116,7 +121,7 @@ fn test_clear_suicided_contract_failed() { assert_noop!( LazyMigrations::clear_suicided_storage( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), vec![contract1_address].try_into().unwrap(), 1000 ), @@ -125,7 +130,7 @@ fn test_clear_suicided_contract_failed() { assert_noop!( LazyMigrations::clear_suicided_storage( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), vec![contract2_address].try_into().unwrap(), 1000 ), @@ -152,7 +157,7 @@ fn test_clear_suicided_empty_input() { let contract_address = mock_contract_with_entries(1, 1, 10); let _ = LazyMigrations::clear_suicided_storage( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), vec![].try_into().unwrap(), 1000, ); @@ -175,7 +180,7 @@ fn test_clear_suicided_contract_multiple_addresses() { // Call the extrinsic to delete the storage entries let _ = LazyMigrations::clear_suicided_storage( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), vec![contract_address1, contract_address2, contract_address3] .try_into() .unwrap(), @@ -206,7 +211,7 @@ fn test_clear_suicided_entry_limit() { let contract_address2 = mock_contract_with_entries(2, 1, 1); let _ = LazyMigrations::clear_suicided_storage( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), vec![contract_address1, contract_address2] .try_into() .unwrap(), @@ -239,7 +244,7 @@ fn test_clear_suicided_mixed_suicided_and_non_suicided() { assert_noop!( LazyMigrations::clear_suicided_storage( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), vec![ contract_address1, contract_address2, @@ -277,7 +282,7 @@ fn test_create_contract_metadata_contract_not_exist() { ExtBuilder::default().build().execute_with(|| { assert_noop!( LazyMigrations::create_contract_metadata( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), address_build(1), ), Error::::ContractNotExist @@ -292,7 +297,7 @@ fn test_create_contract_metadata_success_path() { let address = create_dummy_contract_without_metadata(1); assert_ok!(LazyMigrations::create_contract_metadata( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), address, )); @@ -301,7 +306,7 @@ fn test_create_contract_metadata_success_path() { // Should not be able to set metadata again assert_noop!( LazyMigrations::create_contract_metadata( - RuntimeOrigin::signed(AccountId32::from([45; 32])), + RuntimeOrigin::signed(AccountId::from([45; 20])), address, ), Error::::ContractMetadataAlreadySet @@ -574,3 +579,194 @@ fn test_state_migration_will_migrate_10_000_items() { assert_eq!(total_weight, weight_for(expected_reads, expected_writes)); }) } + +// Helper function to create a foreign asset with basic metadata +fn create_old_foreign_asset(location: Location) -> AssetId { + let asset = MockAssetType::Xcm(location.clone().into()); + let asset_id: AssetId = asset.clone().into(); + // First register asset in asset manager with a Location + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + asset, + 1, + 1, + true, + )); + + // Create the asset through assets pallet + assert_ok!(Assets::create( + RuntimeOrigin::signed(AccountId::from([1; 20])), + asset_id, + AccountId::from([1; 20]).into(), + 1, + )); + + // Set metadata for the asset + assert_ok!(Assets::set_metadata( + RuntimeOrigin::signed(AccountId::from([1; 20])), + asset_id, + b"Test".to_vec(), + b"TEST".to_vec(), + 12, + )); + + asset_id +} + +#[test] +fn test_start_foreign_asset_migration_success() { + ExtBuilder::default().build().execute_with(|| { + let location = xcm::latest::Location::new(1, [xcm::latest::Junction::Parachain(1000)]); + let asset_id = create_old_foreign_asset(location); + + assert_ok!(Assets::mint( + RuntimeOrigin::signed(AccountId::from([1; 20])), + asset_id.into(), + AccountId::from([1; 20]).into(), + 100, + )); + + // Verify asset is live by calling transfer in pallet assets + assert_ok!(Assets::transfer( + RuntimeOrigin::signed(AccountId::from([1; 20])), + asset_id.into(), + AccountId::from([2; 20]).into(), + 100, + )); + + // Try to migrate the asset + assert_ok!(LazyMigrations::start_foreign_assets_migration( + RuntimeOrigin::root(), + asset_id, + )); + + // Verify asset is frozen by calling transfer in pallet assets + assert_noop!( + Assets::transfer( + RuntimeOrigin::signed(AccountId::from([1; 20])), + asset_id.into(), + AccountId::from([2; 20]).into(), + 100, + ), + pallet_assets::Error::::AssetNotLive + ); + + // Verify migration status + match crate::pallet::ForeignAssetMigrationStatusValue::::get() { + ForeignAssetMigrationStatus::Migrating(info) => { + assert_eq!(info.asset_id, asset_id); + assert_eq!(info.remaining_balances, 1); + assert_eq!(info.remaining_approvals, 0); + } + _ => panic!("Expected migration status to be Migrating"), + } + }); +} + +#[test] +fn test_start_foreign_asset_migration_already_migrating() { + ExtBuilder::default().build().execute_with(|| { + let location = xcm::latest::Location::new(1, [xcm::latest::Junction::Parachain(1000)]); + let asset_id = create_old_foreign_asset(location); + + // Start first migrationJunction::Parachain(1000) + assert_ok!(LazyMigrations::start_foreign_assets_migration( + RuntimeOrigin::root(), + asset_id, + )); + + // Try to start another migration while one is in progress + assert_noop!( + LazyMigrations::start_foreign_assets_migration(RuntimeOrigin::root(), 2u128), + Error::::MigrationNotFinished + ); + }); +} + +#[test] +fn test_start_foreign_asset_migration_asset_not_found() { + ExtBuilder::default().build().execute_with(|| { + // Try to migrate non-existent asset + assert_noop!( + LazyMigrations::start_foreign_assets_migration(RuntimeOrigin::root(), 1u128), + Error::::AssetNotFound + ); + }); +} + +#[test] +fn test_start_foreign_asset_migration_asset_type_not_found() { + ExtBuilder::default().build().execute_with(|| { + let asset_id = 1u128; + + // Create asset without registering in asset manager + assert_ok!(Assets::create( + RuntimeOrigin::signed(AccountId::from([1; 20])), + asset_id.into(), + AccountId::from([1; 20]).into(), + 1, + )); + + assert_noop!( + LazyMigrations::start_foreign_assets_migration(RuntimeOrigin::root(), asset_id), + Error::::AssetTypeNotFound + ); + }); +} + +#[test] +fn test_start_foreign_asset_migration_unauthorized() { + ExtBuilder::default().build().execute_with(|| { + let location = xcm::latest::Location::new(1, [xcm::latest::Junction::Parachain(1000)]); + let asset_id = create_old_foreign_asset(location); + + // Try to migrate with non-root origin + assert_noop!( + LazyMigrations::start_foreign_assets_migration( + RuntimeOrigin::signed(AccountId::from([1; 20])), + asset_id + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_start_foreign_asset_migration_with_balances_and_approvals() { + ExtBuilder::default().build().execute_with(|| { + let location = xcm::latest::Location::new(1, [xcm::latest::Junction::Parachain(1000)]); + let asset_id = create_old_foreign_asset(location); + + // Add some balances + assert_ok!(Assets::mint( + RuntimeOrigin::signed(AccountId::from([1; 20])), + asset_id.into(), + AccountId::from([2; 20]).into(), + 100, + )); + + // Add some approvals + assert_ok!(Assets::approve_transfer( + RuntimeOrigin::signed(AccountId::from([2; 20])), + asset_id.into(), + AccountId::from([3; 20]).into(), + 50, + )); + + // Start migration + assert_ok!(LazyMigrations::start_foreign_assets_migration( + RuntimeOrigin::root(), + asset_id, + )); + + // Verify migration status includes the balances and approvals + match crate::pallet::ForeignAssetMigrationStatusValue::::get() { + ForeignAssetMigrationStatus::Migrating(info) => { + assert_eq!(info.asset_id, asset_id); + assert_eq!(info.remaining_balances, 1); + assert_eq!(info.remaining_approvals, 1); + } + _ => panic!("Expected migration status to be Migrating"), + } + }); +}