Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
35 changes: 34 additions & 1 deletion pallets/moonbeam-foreign-assets/src/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@

extern crate alloc;

use crate::{AssetStatus, AssetsById, Call, Config, Pallet};
use crate::{AssetStatus, AssetsById, Call, Config, Pallet, PendingDeposits};
use alloc::format;
use ethereum_types::{H160, U256};
use frame_benchmarking::v2::*;
use frame_support::pallet_prelude::*;
use frame_system::RawOrigin;
Expand Down Expand Up @@ -236,6 +237,38 @@ mod benchmarks {
Ok(())
}

#[benchmark]
fn claim_pending_deposit() -> Result<(), BenchmarkError> {
let asset_id = 1u128;
let symbol = format!("MT{}", asset_id);
let name = format!("Mytoken{}", asset_id);

Pallet::<T>::create_foreign_asset(
RawOrigin::Root.into(),
asset_id,
location_of(asset_id),
18,
str_to_bv(&symbol),
str_to_bv(&name),
)?;

let beneficiary = H160([1u8; 20]);
let amount = U256::from(1_000_000u128);

// Insert a pending deposit directly into storage
PendingDeposits::<T>::insert(asset_id, beneficiary, amount);

let caller: T::AccountId = frame_benchmarking::whitelisted_caller();

#[extrinsic_call]
_(RawOrigin::Signed(caller), asset_id, beneficiary);

// Verify pending deposit was cleared
assert_eq!(PendingDeposits::<T>::get(asset_id, beneficiary), None);

Ok(())
}

impl_benchmark_test_suite! {
Pallet,
crate::benchmarks::tests::new_test_ext(),
Expand Down
137 changes: 110 additions & 27 deletions pallets/moonbeam-foreign-assets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,15 @@ impl<O, Original: EnsureOrigin<O, Success = Location>> EnsureOrigin<O>
pub(crate) struct ForeignAssetsMatcher<T>(core::marker::PhantomData<T>);

impl<T: crate::Config> ForeignAssetsMatcher<T> {
fn match_asset(asset: &Asset) -> Result<(H160, U256, AssetStatus), MatchError> {
fn match_asset(asset: &Asset) -> Result<(AssetId, H160, U256, AssetStatus), MatchError> {
let (amount, location) = match (&asset.fun, &asset.id) {
(Fungibility::Fungible(ref amount), XcmAssetId(ref location)) => (amount, location),
_ => return Err(MatchError::AssetNotHandled),
};

if let Some((asset_id, asset_status)) = AssetsByLocation::<T>::get(&location) {
Ok((
asset_id,
Pallet::<T>::contract_address_from_asset_id(asset_id),
U256::from(*amount),
asset_status,
Expand All @@ -157,6 +158,15 @@ pub enum AssetStatus {
FrozenXcmDepositForbidden,
}

impl AssetStatus {
pub fn is_frozen(&self) -> bool {
matches!(
self,
AssetStatus::FrozenXcmDepositAllowed | AssetStatus::FrozenXcmDepositForbidden
)
}
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EvmForeignAssetInfo {
pub asset_id: AssetId,
Expand Down Expand Up @@ -271,6 +281,8 @@ pub mod pallet {
InvalidSymbol,
InvalidTokenName,
LocationAlreadyExists,
NoPendingDeposit,
AssetNotActive,
TooManyForeignAssets,
}

Expand Down Expand Up @@ -302,6 +314,19 @@ pub mod pallet {
},
/// Tokens have been locked for asset creation
TokensLocked(T::AccountId, AssetId, AssetBalance),
/// A deposit was recorded as pending because the asset is frozen
PendingDepositRecorded {
asset_id: AssetId,
beneficiary: H160,
amount: U256,
total_pending: U256,
},
/// A pending deposit was claimed and minted
PendingDepositClaimed {
asset_id: AssetId,
beneficiary: H160,
amount: U256,
},
}

/// Mapping from an asset id to a Foreign asset type.
Expand All @@ -326,6 +351,13 @@ pub mod pallet {
pub type AssetsCreationDetails<T: Config> =
StorageMap<_, Blake2_128Concat, AssetId, AssetDepositDetails<T>>;

/// Pending deposits for frozen assets, keyed by (asset_id, beneficiary).
/// Deposits for the same (asset_id, beneficiary) accumulate via checked_add.
#[pallet::storage]
#[pallet::getter(fn pending_deposits)]
pub type PendingDeposits<T: Config> =
StorageDoubleMap<_, Blake2_128Concat, AssetId, Blake2_128Concat, H160, U256, OptionQuery>;

#[derive(Clone, Decode, Encode, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
pub struct AssetDepositDetails<T: Config> {
pub deposit_account: T::AccountId,
Expand Down Expand Up @@ -560,6 +592,50 @@ pub mod pallet {

Self::do_unfreeze_asset(asset_id, xcm_location)
}

/// Claim a pending deposit for a given asset and beneficiary.
/// Callable by any signed origin (permissionless). Tokens are minted to the
/// beneficiary, not the caller. Requires the asset to be active (unfrozen).
#[pallet::call_index(4)]
#[pallet::weight(<T as Config>::WeightInfo::claim_pending_deposit())]
pub fn claim_pending_deposit(
origin: OriginFor<T>,
asset_id: AssetId,
beneficiary: H160,
) -> DispatchResult {
ensure_signed(origin)?;

let xcm_location =
AssetsById::<T>::get(&asset_id).ok_or(Error::<T>::AssetDoesNotExist)?;
let (_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;

ensure!(
asset_status == AssetStatus::Active,
Error::<T>::AssetNotActive
);

let amount = PendingDeposits::<T>::get(asset_id, beneficiary)
.ok_or(Error::<T>::NoPendingDeposit)?;

let contract_address = Self::contract_address_from_asset_id(asset_id);

// Both the storage removal and the EVM mint run inside the same
// transactional layer so they are rolled back atomically on failure.
frame_support::storage::with_storage_layer(|| {
PendingDeposits::<T>::remove(asset_id, beneficiary);
EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
})
.map_err(|_| Error::<T>::EvmCallMintIntoFail)?;

Self::deposit_event(Event::PendingDepositClaimed {
asset_id,
beneficiary,
amount,
});

Ok(())
}
}

impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -696,10 +772,7 @@ pub mod pallet {
let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;

ensure!(
asset_status == AssetStatus::Active,
Error::<T>::AssetAlreadyFrozen
);
ensure!(!asset_status.is_frozen(), Error::<T>::AssetAlreadyFrozen);

EvmCaller::<T>::erc20_pause(asset_id)?;

Expand All @@ -722,11 +795,7 @@ pub mod pallet {
let (_asset_id, asset_status) = AssetsByLocation::<T>::get(&xcm_location)
.ok_or(Error::<T>::CorruptedStorageOrphanLocation)?;

ensure!(
asset_status == AssetStatus::FrozenXcmDepositAllowed
|| asset_status == AssetStatus::FrozenXcmDepositForbidden,
Error::<T>::AssetNotFrozen
);
ensure!(asset_status.is_frozen(), Error::<T>::AssetNotFrozen);

EvmCaller::<T>::erc20_unpause(asset_id)?;

Expand All @@ -745,21 +814,39 @@ pub mod pallet {
// we have just traced from which account it should have been withdrawn.
// So we will retrieve these information and make the transfer from the origin account.
fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
let (contract_address, amount, asset_status) =
let (asset_id, contract_address, amount, asset_status) =
ForeignAssetsMatcher::<T>::match_asset(what)?;

if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
return Err(MatchError::AssetNotHandled.into());
return Err(XcmError::FailedToTransactAsset(
"asset is frozen and XCM deposits are forbidden",
));
}

let beneficiary = T::XcmLocationToH160::convert_location(who)
.ok_or(MatchError::AccountIdConversionFailed)?;

// 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) {
let total_pending = PendingDeposits::<T>::get(asset_id, beneficiary)
.unwrap_or(U256::zero())
.checked_add(amount)
.ok_or(XcmError::Overflow)?;

PendingDeposits::<T>::insert(asset_id, beneficiary, total_pending);

Pallet::<T>::deposit_event(Event::PendingDepositRecorded {
asset_id,
beneficiary,
amount,
total_pending,
});
} else {
// We perform the evm transfers in a storage transaction to ensure
// that if it fails any contract storage changes are rolled back.
frame_support::storage::with_storage_layer(|| {
EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount)
})?;
}

Ok(())
}
Expand All @@ -770,13 +857,11 @@ pub mod pallet {
to: &Location,
_context: &XcmContext,
) -> Result<AssetsInHolding, XcmError> {
let (contract_address, amount, asset_status) =
let (_asset_id, contract_address, amount, asset_status) =
ForeignAssetsMatcher::<T>::match_asset(asset)?;

if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
asset_status
{
return Err(MatchError::AssetNotHandled.into());
if asset_status.is_frozen() {
return Err(XcmError::FailedToTransactAsset("asset is frozen"));
}

let from = T::XcmLocationToH160::convert_location(from)
Expand Down Expand Up @@ -806,15 +891,13 @@ pub mod pallet {
who: &Location,
_context: Option<&XcmContext>,
) -> Result<AssetsInHolding, XcmError> {
let (contract_address, amount, asset_status) =
let (_asset_id, contract_address, amount, asset_status) =
ForeignAssetsMatcher::<T>::match_asset(what)?;
let who = T::XcmLocationToH160::convert_location(who)
.ok_or(MatchError::AccountIdConversionFailed)?;

if let AssetStatus::FrozenXcmDepositForbidden | AssetStatus::FrozenXcmDepositAllowed =
asset_status
{
return Err(MatchError::AssetNotHandled.into());
if asset_status.is_frozen() {
return Err(XcmError::FailedToTransactAsset("asset is frozen"));
}

// We perform the evm transfers in a storage transaction to ensure that if it fail
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
Loading
Loading