Lines
100 %
Functions
Branches
// Copyright 2025 Moonbeam Foundation.
// This file is part of Moonbeam.
// Moonbeam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Moonbeam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Moonbeam. If not, see <http://www.gnu.org/licenses/>.
use crate::*;
use mock::*;
use frame_support::traits::Currency;
use frame_support::{assert_noop, assert_ok};
use precompile_utils::testing::Bob;
use xcm::latest::prelude::*;
fn encode_ticker(str_: &str) -> BoundedVec<u8, ConstU32<256>> {
BoundedVec::try_from(str_.as_bytes().to_vec()).expect("too long")
}
fn encode_token_name(str_: &str) -> BoundedVec<u8, ConstU32<256>> {
#[test]
fn create_foreign_and_freeze_unfreeze_using_xcm() {
ExtBuilder::default().build().execute_with(|| {
let deposit = ForeignAssetCreationDeposit::get();
Balances::make_free_balance_be(&PARA_A, deposit);
let asset_location: Location = (Parent, Parachain(1), PalletInstance(13)).into();
// create foreign asset
assert_ok!(EvmForeignAssets::create_foreign_asset(
RuntimeOrigin::signed(PARA_A),
1,
asset_location.clone(),
18,
encode_ticker("MTT"),
encode_token_name("Mytoken"),
));
assert_eq!(
EvmForeignAssets::assets_by_id(1),
Some(asset_location.clone())
);
EvmForeignAssets::assets_by_location(asset_location.clone()),
Some((1, AssetStatus::Active)),
expect_events(vec![Event::ForeignAssetCreated {
contract_address: H160([
255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
]),
asset_id: 1,
xcm_location: asset_location.clone(),
deposit: Some(deposit),
}]);
let (xcm_location, asset_id): (Location, u128) = get_asset_created_hook_invocation()
.expect("Decoding of invocation data should not fail");
assert_eq!(xcm_location, asset_location.clone());
assert_eq!(asset_id, 1u128);
// Check storage
EvmForeignAssets::assets_by_id(&1),
EvmForeignAssets::assets_by_location(&asset_location),
Some((1, AssetStatus::Active))
// Unfreeze should return AssetNotFrozen error
assert_noop!(
EvmForeignAssets::unfreeze_foreign_asset(RuntimeOrigin::signed(PARA_A), 1),
Error::<Test>::AssetNotFrozen
// Freeze should work
assert_ok!(EvmForeignAssets::freeze_foreign_asset(
true
),);
Some((1, AssetStatus::FrozenXcmDepositAllowed))
// Should not be able to freeze an asset already frozen
EvmForeignAssets::freeze_foreign_asset(RuntimeOrigin::signed(PARA_A), 1, true),
Error::<Test>::AssetAlreadyFrozen
// Unfreeze should work
assert_ok!(EvmForeignAssets::unfreeze_foreign_asset(
1
});
fn create_foreign_and_freeze_unfreeze_using_root() {
RuntimeOrigin::root(),
Location::parent(),
assert_eq!(EvmForeignAssets::assets_by_id(1), Some(Location::parent()));
EvmForeignAssets::assets_by_location(Location::parent()),
expect_events(vec![crate::Event::ForeignAssetCreated {
xcm_location: Location::parent(),
deposit: None,
assert_eq!(xcm_location, Location::parent());
assert_eq!(EvmForeignAssets::assets_by_id(&1), Some(Location::parent()));
EvmForeignAssets::assets_by_location(&Location::parent()),
EvmForeignAssets::unfreeze_foreign_asset(RuntimeOrigin::root(), 1),
EvmForeignAssets::freeze_foreign_asset(RuntimeOrigin::root(), 1, true),
fn test_asset_exists_error() {
EvmForeignAssets::assets_by_id(1).unwrap(),
Location::parent()
EvmForeignAssets::create_foreign_asset(
),
Error::<Test>::AssetAlreadyExists
fn test_regular_user_cannot_call_extrinsics() {
RuntimeOrigin::signed(Bob.into()),
sp_runtime::DispatchError::BadOrigin
EvmForeignAssets::change_xcm_location(
fn test_root_can_change_foreign_asset_for_asset_id() {
assert_ok!(EvmForeignAssets::change_xcm_location(
Location::here()
// New associations are stablished
assert_eq!(EvmForeignAssets::assets_by_id(1).unwrap(), Location::here());
EvmForeignAssets::assets_by_location(Location::here()).unwrap(),
(1, AssetStatus::Active),
// Old ones are deleted
assert!(EvmForeignAssets::assets_by_location(Location::parent()).is_none());
expect_events(vec![
crate::Event::ForeignAssetCreated {
},
crate::Event::ForeignAssetXcmLocationChanged {
previous_xcm_location: Location::parent(),
new_xcm_location: Location::here(),
])
fn test_asset_id_non_existent_error() {
EvmForeignAssets::change_xcm_location(RuntimeOrigin::root(), 1, Location::parent()),
Error::<Test>::AssetDoesNotExist
fn test_location_already_exist_error() {
// Setup: create a first foreign asset taht we will try to override
2,
Error::<Test>::LocationAlreadyExists
// Setup: create a second foreign asset that will try to override the first one
Location::new(2, *&[]),
EvmForeignAssets::change_xcm_location(RuntimeOrigin::root(), 2, Location::parent()),
fn test_governance_can_change_any_asset_location() {
Balances::make_free_balance_be(&PARA_C, deposit + 10);
let asset_location: Location = (Parent, Parachain(3), PalletInstance(22)).into();
let asset_id = 5;
// create foreign asset using para c
RuntimeOrigin::signed(PARA_C),
asset_id,
10,
encode_ticker("PARC"),
encode_token_name("Parachain C Token"),
assert_eq!(Balances::free_balance(&PARA_C), 10);
EvmForeignAssets::assets_by_id(asset_id),
EvmForeignAssets::assets_by_location(asset_location),
Some((asset_id, AssetStatus::Active)),
// This asset doesn't belong to PARA A, so it should not be able to change the location
EvmForeignAssets::freeze_foreign_asset(RuntimeOrigin::signed(PARA_A), asset_id, true),
Error::<Test>::LocationOutsideOfOrigin,
let new_asset_location: Location = (Parent, Parachain(1), PalletInstance(1)).into();
// Also PARA A cannot change the location
new_asset_location.clone(),
// Change location using root, now PARA A can control this asset
Some(new_asset_location.clone())
EvmForeignAssets::assets_by_location(new_asset_location),
// Freeze will not work since this asset has been moved from PARA C to PARA A
EvmForeignAssets::freeze_foreign_asset(RuntimeOrigin::signed(PARA_C), asset_id, true),
// But if we try using PARA A, it should work
fn xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset() {
let asset_location = Location::parent();
let beneficiary_location = Location::new(
0,
[AccountKey20 {
network: None,
key: [1u8; 20],
}],
let beneficiary_h160 = H160([1u8; 20]);
// Create foreign asset (deploys the ERC20 contract)
let xcm_asset = xcm::latest::Asset {
id: xcm::latest::AssetId(asset_location.clone()),
fun: Fungibility::Fungible(100),
};
// Deposit succeeds on an active (unpaused) asset — mints directly via EVM
assert_ok!(
<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
&xcm_asset,
&beneficiary_location,
None,
)
// No pending deposit for active asset
EvmForeignAssets::pending_deposits(1, beneficiary_h160),
None
// Freeze with allow_xcm_deposit = true
true,
// Deposit succeeds but goes to PendingDeposits storage (not EVM mint)
Some(U256::from(100))
fn pending_deposits_accumulate() {
let xcm_asset_100 = xcm::latest::Asset {
let xcm_asset_200 = xcm::latest::Asset {
fun: Fungibility::Fungible(200),
// First deposit
&xcm_asset_100,
// Second deposit accumulates
&xcm_asset_200,
Some(U256::from(300))
fn pending_deposits_overflow() {
// Seed pending deposits to U256::MAX directly
crate::PendingDeposits::<Test>::insert(1, beneficiary_h160, U256::MAX);
// Any further deposit should overflow
let xcm_asset_one = xcm::latest::Asset {
fun: Fungibility::Fungible(1),
let result = <EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
&xcm_asset_one,
assert_eq!(result, Err(xcm::latest::Error::Overflow.into()));
// Pending deposit unchanged
Some(U256::MAX)
fn claim_pending_deposit_success() {
fun: Fungibility::Fungible(500),
// Deposit goes to pending
Some(U256::from(500))
// Unfreeze the asset
// Verify balance is zero before claim
let balance_before =
crate::evm::EvmCaller::<Test>::erc20_balance_of(1, beneficiary_h160).unwrap();
assert_eq!(balance_before, U256::zero());
// Claim the pending deposit (any signed origin can call this)
assert_ok!(EvmForeignAssets::claim_pending_deposit(
beneficiary_h160,
// Pending deposit should be cleared
// Verify beneficiary received the tokens
let balance_after =
assert_eq!(balance_after, U256::from(500));
fn claim_pending_deposit_fails_when_asset_frozen() {
// Freeze
// Claim fails because asset is still frozen
EvmForeignAssets::claim_pending_deposit(
Error::<Test>::AssetNotActive
fn claim_pending_deposit_fails_when_no_pending() {
// No pending deposit exists — claim fails
Error::<Test>::NoPendingDeposit
fn xcm_deposit_blocked_on_frozen_xcm_deposit_forbidden_asset() {
// Verifies that deposit_asset correctly rejects deposits when the asset
// status is FrozenXcmDepositForbidden (blocked at the pallet level before
// reaching the EVM).
// Create foreign asset
// Freeze with allow_xcm_deposit = false
false,
Some((1, AssetStatus::FrozenXcmDepositForbidden))
// Deposit is rejected at the pallet level (before EVM call)
result,
Err(XcmError::FailedToTransactAsset(
"asset is frozen and XCM deposits are forbidden"
)),