Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions pallets/moonbeam-foreign-assets/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use precompile_utils::solidity::Codec;
use precompile_utils_macro::keccak256;
use sp_runtime::traits::ConstU32;
use sp_runtime::{DispatchError, SaturatedConversion};
use sp_std::vec;
use sp_std::vec::Vec;
use xcm::latest::Error as XcmError;

Expand All @@ -38,12 +39,19 @@ const ERC20_CREATE_MAX_CALLDATA_SIZE: usize = 16 * 1024; // 16Ko
const ERC20_CREATE_GAS_LIMIT: u64 = 3_600_000; // highest failure: 3_600_000
pub(crate) const ERC20_BURN_FROM_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
pub(crate) const ERC20_MINT_INTO_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
const ERC20_MINT_INTO_PAUSED_GAS_LIMIT: u64 = 600_000; // unpause + mintInto + pause via batch
const ERC20_PAUSE_GAS_LIMIT: u64 = 160_000; // highest failure: 150_500
pub(crate) const ERC20_TRANSFER_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
pub(crate) const ERC20_APPROVE_GAS_LIMIT: u64 = 160_000; // highest failure: 153_000
const ERC20_UNPAUSE_GAS_LIMIT: u64 = 160_000; // highest failure: 149_500
pub(crate) const ERC20_BALANCE_OF_GAS_LIMIT: u64 = 160_000; // Calculated effective gas: max(used: 24276, pov: 150736, storage: 0) = 150736

// Batch precompile at 0x0000000000000000000000000000000000000808
const BATCH_PRECOMPILE_ADDRESS: H160 = H160([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x08,
]);

#[derive(Debug, PartialEq)]
pub enum EvmError {
BurnFromFail(String),
Expand Down Expand Up @@ -215,6 +223,90 @@ impl<T: crate::Config> EvmCaller<T> {
Ok(())
}

/// Mint tokens into a beneficiary's account on a paused ERC20 contract.
/// Encodes a single batchAll call to the batch precompile that atomically
/// executes: unpause → mintInto → pause.
pub(crate) fn erc20_mint_into_paused(
erc20_contract_address: H160,
beneficiary: H160,
amount: U256,
) -> Result<(), EvmError> {
// Build call data for each subcall
let mut unpause_data = Vec::with_capacity(4);
unpause_data.extend_from_slice(&keccak256!("unpause()")[..4]);

let mut mint_data = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
mint_data.extend_from_slice(&keccak256!("mintInto(address,uint256)")[..4]);
mint_data.extend_from_slice(H256::from(beneficiary).as_bytes());
mint_data.extend_from_slice(H256::from_uint(&amount).as_bytes());

let mut pause_data = Vec::with_capacity(4);
pause_data.extend_from_slice(&keccak256!("pause()")[..4]);

// Encode batchAll(address[],uint256[],bytes[],uint64[]) targeting the batch precompile
let batch_all_hash = keccak256!("batchAll(address[],uint256[],bytes[],uint64[])");
let batch_all_selector = u32::from_be_bytes([
batch_all_hash[0],
batch_all_hash[1],
batch_all_hash[2],
batch_all_hash[3],
]);
let batch_input =
precompile_utils::solidity::codec::Writer::new_with_selector(batch_all_selector)
.write(BoundedVec::<Address, ConstU32<3>>::from(vec![
Address(erc20_contract_address),
Address(erc20_contract_address),
Address(erc20_contract_address),
]))
.write(BoundedVec::<U256, ConstU32<3>>::from(vec![
U256::zero(),
U256::zero(),
U256::zero(),
]))
.write(BoundedVec::<UnboundedBytes, ConstU32<3>>::from(vec![
unpause_data.into(),
mint_data.into(),
pause_data.into(),
]))
.write(BoundedVec::<u64, ConstU32<3>>::from(vec![0u64, 0u64, 0u64]))
.build();

let weight_limit: Weight =
T::GasWeightMapping::gas_to_weight(ERC20_MINT_INTO_PAUSED_GAS_LIMIT, true);

let exec_info = T::EvmRunner::call(
Pallet::<T>::account_id(),
BATCH_PRECOMPILE_ADDRESS,
batch_input,
U256::default(),
ERC20_MINT_INTO_PAUSED_GAS_LIMIT,
None,
None,
None,
Default::default(),
Default::default(),
false,
false,
Some(weight_limit),
Some(0),
&<T as pallet_evm::Config>::config(),
)
.map_err(|err| EvmError::MintIntoFail(format!("{:?}", err.error.into())))?;

ensure!(
matches!(
exec_info.exit_reason,
ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
),
{
let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
EvmError::MintIntoFail(err)
}
);

Ok(())
}

pub(crate) fn erc20_transfer(
erc20_contract_address: H160,
from: H160,
Expand Down
6 changes: 5 additions & 1 deletion pallets/moonbeam-foreign-assets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,11 @@ pub mod pallet {
// We perform the evm transfers in a storage transaction to ensure that if it fail
// any contract storage changes are rolled back.
frame_support::storage::with_storage_layer(|| {
EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
if matches!(asset_status, AssetStatus::FrozenXcmDepositAllowed) {
EvmCaller::<T>::erc20_mint_into_paused(contract_address, beneficiary, amount)
} else {
EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
}
})?;

Ok(())
Expand Down
12 changes: 11 additions & 1 deletion pallets/moonbeam-foreign-assets/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ impl xcm_executor::traits::ConvertLocation<AccountId> for SiblingAccountOf {
}
}

pub struct MockLocationToH160;
impl xcm_executor::traits::ConvertLocation<H160> for MockLocationToH160 {
fn convert_location(location: &Location) -> Option<H160> {
match location.unpack() {
(0, [Junction::AccountKey20 { network: _, key }]) => Some(H160::from(*key)),
_ => None,
}
}
}

pub struct SiblingOrigin;
impl EnsureOrigin<<Test as frame_system::Config>::RuntimeOrigin> for SiblingOrigin {
type Success = Location;
Expand Down Expand Up @@ -268,7 +278,7 @@ impl crate::Config for Test {
type OnForeignAssetCreated = NoteDownHook<Location>;
type MaxForeignAssets = ConstU32<3>;
type WeightInfo = ();
type XcmLocationToH160 = ();
type XcmLocationToH160 = MockLocationToH160;
type ForeignAssetCreationDeposit = ForeignAssetCreationDeposit;
type Balance = Balance;

Expand Down
112 changes: 112 additions & 0 deletions pallets/moonbeam-foreign-assets/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,115 @@ fn test_governance_can_change_any_asset_location() {
));
});
}

#[test]
fn xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset() {
ExtBuilder::default().build().execute_with(|| {
let asset_location = Location::parent();
let beneficiary_location = Location::new(
0,
[AccountKey20 {
network: None,
key: [1u8; 20],
}],
);

// Create foreign asset (deploys the ERC20 contract)
assert_ok!(EvmForeignAssets::create_foreign_asset(
RuntimeOrigin::root(),
1,
asset_location.clone(),
18,
encode_ticker("MTT"),
encode_token_name("Mytoken"),
));

let xcm_asset = xcm::latest::Asset {
id: xcm::latest::AssetId(asset_location.clone()),
fun: Fungibility::Fungible(100),
};

// Deposit succeeds on an active (unpaused) asset
assert_ok!(
<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
&xcm_asset,
&beneficiary_location,
None,
)
);

// Freeze with allow_xcm_deposit = true
assert_ok!(EvmForeignAssets::freeze_foreign_asset(
RuntimeOrigin::root(),
1,
true,
));
assert_eq!(
EvmForeignAssets::assets_by_location(&asset_location),
Some((1, AssetStatus::FrozenXcmDepositAllowed))
);

// Deposit must still succeed for FrozenXcmDepositAllowed
assert_ok!(
<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
&xcm_asset,
&beneficiary_location,
None,
)
);
});
}

#[test]
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).
ExtBuilder::default().build().execute_with(|| {
let asset_location = Location::parent();
let beneficiary_location = Location::new(
0,
[AccountKey20 {
network: None,
key: [1u8; 20],
}],
);

// Create foreign asset
assert_ok!(EvmForeignAssets::create_foreign_asset(
RuntimeOrigin::root(),
1,
asset_location.clone(),
18,
encode_ticker("MTT"),
encode_token_name("Mytoken"),
));

let xcm_asset = xcm::latest::Asset {
id: xcm::latest::AssetId(asset_location.clone()),
fun: Fungibility::Fungible(100),
};

// Freeze with allow_xcm_deposit = false
assert_ok!(EvmForeignAssets::freeze_foreign_asset(
RuntimeOrigin::root(),
1,
false,
));
assert_eq!(
EvmForeignAssets::assets_by_location(&asset_location),
Some((1, AssetStatus::FrozenXcmDepositForbidden))
);

// Deposit is rejected at the pallet level (before EVM call)
let result = <EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
&xcm_asset,
&beneficiary_location,
None,
);
assert!(
result.is_err(),
"Expected deposit to be rejected for FrozenXcmDepositForbidden, got: {result:?}",
);
});
}
Loading