Skip to content

Comments

Allow XCM deposits on frozen assets#3674

Open
manuelmauro wants to merge 15 commits intomasterfrom
manuel/allow-xcm-deposits-on-freeze
Open

Allow XCM deposits on frozen assets#3674
manuelmauro wants to merge 15 commits intomasterfrom
manuel/allow-xcm-deposits-on-freeze

Conversation

@manuelmauro
Copy link
Contributor

@manuelmauro manuelmauro commented Feb 17, 2026

Summary

  • When a foreign asset is frozen with allow_xcm_deposit = true (FrozenXcmDepositAllowed), incoming XCM deposits are no longer rejected — instead they are tracked in a new PendingDeposits storage
    map, keyed by (asset_id, beneficiary), with amounts accumulating via checked_add
  • Once the asset is unfrozen, anyone can call the new permissionless claim_pending_deposit extrinsic to mint the accrued tokens to the beneficiary via the ERC-20 contract
  • Assets frozen with allow_xcm_deposit = false (FrozenXcmDepositForbidden) continue to reject deposits as before

Changes

pallets/moonbeam-foreign-assets/src/lib.rs

  • Added PendingDeposits StorageDoubleMap<AssetId, H160, U256>
  • Added claim_pending_deposit extrinsic (call index 4) — requires asset to be Active, takes pending amount, mints via EVM, emits PendingDepositClaimed
  • Modified deposit_asset to store pending deposits when status is FrozenXcmDepositAllowed instead of calling the EVM, emitting PendingDepositRecorded
  • Extended ForeignAssetsMatcher::match_asset return type to include AssetId
  • New errors: NoPendingDeposit, AssetNotActive
  • New events: PendingDepositRecorded, PendingDepositClaimed

pallets/moonbeam-foreign-assets/src/tests.rs — 6 new tests:

  • xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset
  • pending_deposits_accumulate
  • claim_pending_deposit_success
  • claim_pending_deposit_fails_when_asset_frozen
  • claim_pending_deposit_fails_when_no_pending
  • xcm_deposit_blocked_on_frozen_xcm_deposit_forbidden_asset

pallets/moonbeam-foreign-assets/src/benchmarks.rs — new claim_pending_deposit benchmark

Weights — updated for all three runtimes (moonbase, moonbeam, moonriver) and the pallet default

Test plan

  • Pallet unit tests pass (cargo test -p pallet-moonbeam-foreign-assets)
  • Verify XCM deposit on FrozenXcmDepositAllowed asset records a pending deposit (no EVM mint)
  • Verify pending deposits accumulate for the same (asset_id, beneficiary)
  • Verify claim_pending_deposit mints tokens and clears storage after unfreeze
  • Verify claim_pending_deposit fails with AssetNotActive while asset is frozen
  • Verify claim_pending_deposit fails with NoPendingDeposit when no deposit exists
  • Verify FrozenXcmDepositForbidden still rejects deposits outright

@manuelmauro manuelmauro self-assigned this Feb 17, 2026
@manuelmauro manuelmauro added B0-silent Changes should not be mentioned in any release notes D5-nicetohaveaudit⚠️ PR contains trivial changes to logic that should be properly reviewed. not-breaking Does not need to be mentioned in breaking changes labels Feb 17, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Adds a pending-deposit mechanism: XCM deposits to assets frozen with FrozenXcmDepositAllowed are recorded in PendingDeposits; claim_pending_deposit mints accumulated deposits once the asset becomes Active. Updates include storage, events, errors, extrinsic, weights, benchmarks, tests, and mocks.

Changes

Cohort / File(s) Summary
Core Pallet Logic
pallets/moonbeam-foreign-assets/src/lib.rs
ForeignAssetsMatcher::match_asset now returns (AssetId, H160, U256, AssetStatus). Added PendingDeposits StorageDoubleMap with getter, claim_pending_deposit call, new events (PendingDepositRecorded, PendingDepositClaimed), new errors (NoPendingDeposit, AssetNotActive), AssetStatus::is_frozen helper, and deposit flow branching to record/claim pending deposits.
Mocks
pallets/moonbeam-foreign-assets/src/mock.rs
Added MockLocationToH160 implementing ConvertLocation<H160> and registered it as XcmLocationToH160 in the Test config.
Tests
pallets/moonbeam-foreign-assets/src/tests.rs, test/suites/dev/moonbase/test-assets/test-foreign-assets-xcm-pending-deposits.ts
Comprehensive unit/integration tests and an e2e JS test covering deposit routing to pending, accumulation/overflow, claim success/failure, freeze/unfreeze flows, and access control.
Benchmarks
pallets/moonbeam-foreign-assets/src/benchmarks.rs
Added claim_pending_deposit benchmark and imports for PendingDeposits, H160, U256.
Pallet Weights
pallets/moonbeam-foreign-assets/src/weights.rs
Added fn claim_pending_deposit() -> Weight to WeightInfo and implementations with detailed storage proof annotations (19 reads, 7 writes).
Runtime Weights
runtime/moonbase/.../pallet_moonbeam_foreign_assets.rs, runtime/moonbeam/.../pallet_moonbeam_foreign_assets.rs, runtime/moonriver/.../pallet_moonbeam_foreign_assets.rs
Added corresponding claim_pending_deposit() weight functions and proof metadata for each runtime weight file.

Sequence Diagram

sequenceDiagram
    participant XCM as XCM Sender
    participant Pallet as ForeignAssets Pallet
    participant Storage as PendingDeposits
    participant Assets as Assets Storage

    rect rgba(200, 150, 255, 0.5)
    note over XCM,Pallet: XCM deposit while asset is FrozenXcmDepositAllowed
    XCM->>Pallet: deposit_asset(location, amount, beneficiary)
    Pallet->>Assets: match_asset(location) -> (asset_id, owner, amount, status)
    Pallet->>Storage: PendingDeposits[asset_id, beneficiary] += amount
    Pallet->>Pallet: Emit PendingDepositRecorded
    end

    rect rgba(150, 200, 255, 0.5)
    note over XCM,Assets: Asset status changed to Active (unfreeze)
    XCM->>Assets: unfreeze_asset(asset_id)
    end

    rect rgba(150, 255, 150, 0.5)
    note over Pallet,Storage: Claim pending deposit
    XCM->>Pallet: claim_pending_deposit(asset_id, beneficiary)
    Pallet->>Assets: verify status == Active
    Pallet->>Storage: read & remove PendingDeposits[asset_id, beneficiary]
    Pallet->>Assets: mint amount to beneficiary
    Pallet->>Pallet: Emit PendingDepositClaimed
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

I4-tests 🎯, B7-runtimenoteworthy, weights-updates

Suggested reviewers

  • librelois
  • stiiifff
  • arturgontijo
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: allowing XCM deposits on frozen assets, which is the primary objective of this PR.
Description check ✅ Passed The description thoroughly details the PR's objectives, changes, and testing approach, covering all major modifications across multiple files and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch manuel/allow-xcm-deposits-on-freeze

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 17, 2026

Coverage Report

@@                           Coverage Diff                           @@
##           master   manuel/allow-xcm-deposits-on-freeze      +/-   ##
=======================================================================
+ Coverage   77.10%                                77.16%   +0.06%     
  Files         389                                   389              
+ Lines       76941                                 77238     +297     
=======================================================================
+ Hits        59321                                 59594     +273     
+ Misses      17620                                 17644      +24     
Files Changed Coverage
/pallets/moonbeam-foreign-assets/src/lib.rs 83.11% (+2.50%) 🔼
/pallets/moonbeam-foreign-assets/src/mock.rs 88.64% (-0.52%) 🔽

Coverage generated Fri Feb 20 17:49:07 UTC 2026

@librelois librelois added the agent-review Trigger CodeRabbit Agent review label Feb 17, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
pallets/moonbeam-foreign-assets/src/tests.rs (1)

426-481: Consider asserting beneficiary balance and post-deposit pause state.

The test verifies the deposit call succeeds, but doesn't assert that:

  1. The beneficiary actually received the minted tokens (e.g., balance_of == 200 after two deposits of 100).
  2. The ERC-20 contract is still paused after the temporary unpause/mint/pause sequence, confirming the re-pause worked.

Adding these assertions would make the test more robust against a scenario where deposit_asset returns Ok(()) but the unpause/mint/pause sequence silently breaks (e.g., re-pause fails but the error is swallowed).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/tests.rs` around lines 426 - 481, Add
assertions that the beneficiary's ERC-20 balance increased and that the ERC-20
contract remains paused after the deposit/unpause/mint/pause sequence: after the
two successful calls to <EvmForeignAssets as
xcm_executor::traits::TransactAsset>::deposit_asset use the beneficiary address
derived from beneficiary_location (the AccountKey20 [1u8;20]) to call the
deployed ERC‑20 contract's balance_of and assert it equals 200 (100 per
deposit), and call the ERC‑20 contract's paused (or is_paused) view and assert
it returns true; keep references to EvmForeignAssets::deposit_asset,
EvmForeignAssets::freeze_foreign_asset, and EvmForeignAssets::assets_by_location
to locate relevant setup.
pallets/moonbeam-foreign-assets/src/lib.rs (1)

759-777: Solid atomicity design — unpause/mint/pause inside a single storage transaction.

The with_storage_layer wrapper correctly ensures that if the mint or re-pause fails, the unpause is rolled back, preventing the contract from being left in an unpaused state. The needs_unpause flag cleanly skips the unpause/pause calls for active assets.

One minor observation: the .map_err(|_| ...) on lines 766 and 773 discards the original Error<T> from erc20_unpause/erc20_pause. This is fine for XCM context (callers only see an opaque XCM error), but if debugging becomes difficult, consider logging the original error before discarding it.

💡 Optional: log before discarding the error
 				if needs_unpause {
 					EvmCaller::<T>::erc20_unpause(asset_id)
-						.map_err(|_| evm::EvmError::EvmCallFail("unpause fail".into()))?;
+						.map_err(|e| {
+							log::debug!("erc20_unpause failed during XCM deposit: {:?}", e);
+							evm::EvmError::EvmCallFail("unpause fail".into())
+						})?;
 				}
 				if needs_unpause {
 					EvmCaller::<T>::erc20_pause(asset_id)
-						.map_err(|_| evm::EvmError::EvmCallFail("pause fail".into()))?;
+						.map_err(|e| {
+							log::debug!("erc20_pause failed during XCM deposit: {:?}", e);
+							evm::EvmError::EvmCallFail("pause fail".into())
+						})?;
 				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/lib.rs` around lines 759 - 777, The error
mapping for EvmCaller::<T>::erc20_unpause and EvmCaller::<T>::erc20_pause is
currently discarding the original Error<T> via .map_err(|_| ...); update these
call sites so the original error is preserved or logged before converting to
evm::EvmError (e.g., capture the err in the closure and call a logger or include
its Display in the evm::EvmError::EvmCallFail message) within the
with_storage_layer transaction around erc20_unpause/erc20_mint_into/erc20_pause
so debugging retains the underlying error details while keeping the same
external error type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pallets/moonbeam-foreign-assets/src/lib.rs`:
- Around line 759-777: The error mapping for EvmCaller::<T>::erc20_unpause and
EvmCaller::<T>::erc20_pause is currently discarding the original Error<T> via
.map_err(|_| ...); update these call sites so the original error is preserved or
logged before converting to evm::EvmError (e.g., capture the err in the closure
and call a logger or include its Display in the evm::EvmError::EvmCallFail
message) within the with_storage_layer transaction around
erc20_unpause/erc20_mint_into/erc20_pause so debugging retains the underlying
error details while keeping the same external error type.

In `@pallets/moonbeam-foreign-assets/src/tests.rs`:
- Around line 426-481: Add assertions that the beneficiary's ERC-20 balance
increased and that the ERC-20 contract remains paused after the
deposit/unpause/mint/pause sequence: after the two successful calls to
<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset use the
beneficiary address derived from beneficiary_location (the AccountKey20
[1u8;20]) to call the deployed ERC‑20 contract's balance_of and assert it equals
200 (100 per deposit), and call the ERC‑20 contract's paused (or is_paused) view
and assert it returns true; keep references to EvmForeignAssets::deposit_asset,
EvmForeignAssets::freeze_foreign_asset, and EvmForeignAssets::assets_by_location
to locate relevant setup.

Copy link
Collaborator

@librelois librelois left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will produce 3 eth-xcm transactions under the hood, it's better to create a new EvmCaller method erc20_mint_into_paused that create an evm call that call the batch precompile under the hood. You will have to write the code that encode the batch call properly

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pallets/moonbeam-foreign-assets/src/evm.rs (1)

93-96: ⚠️ Potential issue | 🟡 Minor

Stray + in the MintIntoFail error string is a debug artifact that now surfaces on the new path.

Line 95: "Erc20 contract call mintInto fail+" — the trailing + is misleading in production logs and is now reachable via both erc20_mint_into and the new erc20_mint_into_paused path.

🐛 Proposed fix
-        XcmError::FailedToTransactAsset("Erc20 contract call mintInto fail+")
+        XcmError::FailedToTransactAsset("Erc20 contract call mintInto fail")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/evm.rs` around lines 93 - 96, The
EvmError::MintIntoFail match arm returns an XcmError::FailedToTransactAsset with
a message containing a stray '+'; update the message in the MintIntoFail branch
to remove the trailing plus so it reads e.g. "Erc20 contract call mintInto fail"
(keep the log::debug line unchanged), ensuring the
XcmError::FailedToTransactAsset invocation inside the EvmError::MintIntoFail arm
is corrected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pallets/moonbeam-foreign-assets/src/evm.rs`:
- Around line 41-52: Add a measured "highest failure: X" comment to the
ERC20_MINT_INTO_PAUSED_GAS_LIMIT constant so it matches the other ERC20_* gas
constants (replace the current descriptive breakdown with a single measured
max-failure value), and extract the BATCH_PRECOMPILE_ADDRESS raw H160 bytes into
a single shared public constant (e.g., pub const BATCH_PRECOMPILE_ADDRESS: H160)
in a common crate/module used by both evm.rs and the RPC/service code; update
references in evm.rs (and other places using the hex literal) to import and use
that shared constant to prevent duplication.
- Around line 225-307: The XCM DepositAsset weighing ignores the expensive
paused-asset path: when FrozenXcmDepositAllowed assets call
erc20_mint_into_paused() they use ~600k gas vs ~160k for normal deposits, but
deposit_asset() still uses the hardcoded DepositAsset weight; update
deposit_asset() to account for the paused-mint path by using the existing
weight_of_erc20_mint() helper or by branching on the asset's
FrozenXcmDepositAllowed status and applying a worst-case weight (or the
dynamically computed weight) so XCM fees and message weight reflect the 3.75×
cost; touch functions/methods deposit_asset, erc20_mint_into_paused,
weight_of_erc20_mint and the FrozenXcmDepositAllowed check to implement the
conditional weight selection and ensure XCM fee calculation uses the adjusted
weight.

---

Outside diff comments:
In `@pallets/moonbeam-foreign-assets/src/evm.rs`:
- Around line 93-96: The EvmError::MintIntoFail match arm returns an
XcmError::FailedToTransactAsset with a message containing a stray '+'; update
the message in the MintIntoFail branch to remove the trailing plus so it reads
e.g. "Erc20 contract call mintInto fail" (keep the log::debug line unchanged),
ensuring the XcmError::FailedToTransactAsset invocation inside the
EvmError::MintIntoFail arm is corrected.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pallets/moonbeam-foreign-assets/src/evm.rs`:
- Around line 247-253: The computed selector for batchAll is wrong: update the
computation so it matches the selector used in Batch.sol (0x96e292b8) — either
by changing the keccak256! input string to the exact Solidity function signature
Batch.sol uses for batchAll or by hardcoding the selector constant to
0x96e292b8; modify the symbols batch_all_hash and batch_all_selector (and the
keccak256! invocation) so the resulting u32 equals 0x96e292b8, ensuring
precompile dispatch for batchAll(address[],uint256[],bytes[],uint64[]) matches
the Solidity contract.

---

Duplicate comments:
In `@pallets/moonbeam-foreign-assets/src/evm.rs`:
- Around line 49-53: BATCH_PRECOMPILE_ADDRESS is hard-coded here as raw H160
bytes and likely duplicated elsewhere; extract this value into a single shared
constant (e.g., a pub const BATCH_PRECOMPILE_ADDRESS: H160 in a common
crate/module) and replace local definitions in evm.rs and other copies with an
import/use of that shared symbol so all locations reference the single canonical
value and avoid silent drift.
- Line 42: The comment for the constant ERC20_MINT_INTO_PAUSED_GAS_LIMIT does
not follow the project convention of annotating the measured worst-case failure
gas (e.g., "// highest failure: 154_000"); run a worst-case gas measurement for
the sequence represented by ERC20_MINT_INTO_PAUSED_GAS_LIMIT (unpause + mintInto
+ pause via batch) and update the trailing comment to the canonical form "//
highest failure: <measured_value>" using the measured numeric value so it
matches the other ERC20_*_GAS_LIMIT constants.
- Around line 226-308: The deposit_asset flow currently uses a hardcoded weight
that ignores the paused-mint (erc20_mint_into_paused) path which consumes ~3.75×
more gas; update deposit_asset to branch on the FrozenXcmDepositAllowed status
(or otherwise detect when erc20_mint_into_paused will be invoked) and use
T::GasWeightMapping::gas_to_weight with ERC20_MINT_INTO_PAUSED_GAS_LIMIT for
that path (and the existing lower gas limit for the normal path), or add a
separate weight constant for the paused-mint branch and apply it when calling
erc20_mint_into_paused so XCM DepositAsset weights reflect the real gas usage.
Ensure the branch references the FrozenXcmDepositAllowed check and the
ERC20_MINT_INTO_PAUSED_GAS_LIMIT/erc20_mint_into_paused symbols so the correct
weight is selected.

@librelois
Copy link
Collaborator

@manuelmauro I have an idea for a better solution: create a new double storage map to store the received tokens when the asset is paused, and add a new extrinsic to claim these tokens once the asset is unpaused. This way, we don’t need extra EVM calls during XCM execution or any changes to the ERC20 contract bytecode.

@manuelmauro
Copy link
Contributor Author

@manuelmauro I have an idea for a better solution: create a new double storage map to store the received tokens when the asset is paused, and add a new extrinsic to claim these tokens once the asset is unpaused. This way, we don’t need extra EVM calls during XCM execution or any changes to the ERC20 contract bytecode.

Nice idea! Addressed in 4a869aa

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
pallets/moonbeam-foreign-assets/src/lib.rs (2)

345-350: PendingDeposits storage is unbounded — consider documenting the growth model.

The StorageDoubleMap has no bounds on the number of entries. While each (asset_id, beneficiary) pair stores a single accumulating U256, the number of unique beneficiaries receiving XCM deposits while an asset is frozen could grow over time. Entries persist until individually claimed via claim_pending_deposit.

This is inherent to the design, but worth documenting explicitly (e.g., in a doc comment on the storage item) so future maintainers understand the expected growth pattern and can plan governance-level mitigations if needed (e.g., batch claim tooling).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/lib.rs` around lines 345 - 350, Add a doc
comment to the PendingDeposits storage declaration explaining that
StorageDoubleMap<..., AssetId, ..., H160, U256, ...> is unbounded in the number
of (asset_id, beneficiary) entries, that values accumulate via checked_add and
persist until claim_pending_deposit is called, and describe the expected growth
model and operational mitigations (e.g., governance-level batch claim tooling or
periodic cleanup) so future maintainers understand the persistence and scaling
implications.

586-626: claim_pending_deposit — add a comment documenting the transactional guarantee for the take + mint sequence.

PendingDeposits::take (line 609) removes the deposit from storage before the EVM mint (line 614–617). If the mint fails, the operation is safe only because #[pallet::call] wraps the entire dispatchable in frame_support::storage::with_storage_layer, rolling back all storage mutations on error. This transactional guarantee is implicit and relies on FRAME's default behavior; adding a brief comment near the take would clarify this invariant for future maintainers and prevent accidental breakage if the code is refactored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/lib.rs` around lines 586 - 626, The code
removes the pending deposit with PendingDeposits::take before calling
EvmCaller::erc20_mint_into, relying on
frame_support::storage::with_storage_layer to roll back storage on EVM call
failure; add a concise comment in claim_pending_deposit next to the
PendingDeposits::take line documenting that the take happens prior to the mint
and that the surrounding frame_support::storage::with_storage_layer provides the
transactional rollback guarantee so future changes won't accidentally break this
invariant.
pallets/moonbeam-foreign-assets/src/tests.rs (1)

494-558: Consider adding an overflow test for pending deposit accumulation.

The deposit_asset code uses checked_add and returns XcmError::Overflow on U256 overflow, but there's no test exercising this path. While U256 overflow is practically unlikely, testing the error branch improves confidence in the checked arithmetic handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/tests.rs` around lines 494 - 558, Add a
unit test that exercises the U256 overflow path in
EvmForeignAssets::deposit_asset: create the foreign asset and freeze it (as in
pending_deposits_accumulate), set the pending_deposits for a beneficiary to
U256::MAX (or perform a deposit of U256::MAX) and then call deposit_asset with
any positive amount (e.g. 1) and assert it returns
Err(xcm::latest::XcmError::Overflow) and that pending_deposits is unchanged;
name the test e.g. pending_deposits_overflow and reuse symbols from the existing
test (EvmForeignAssets::create_foreign_asset,
EvmForeignAssets::freeze_foreign_asset, <EvmForeignAssets as
xcm_executor::traits::TransactAsset>::deposit_asset,
EvmForeignAssets::pending_deposits).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pallets/moonbeam-foreign-assets/src/lib.rs`:
- Around line 345-350: Add a doc comment to the PendingDeposits storage
declaration explaining that StorageDoubleMap<..., AssetId, ..., H160, U256, ...>
is unbounded in the number of (asset_id, beneficiary) entries, that values
accumulate via checked_add and persist until claim_pending_deposit is called,
and describe the expected growth model and operational mitigations (e.g.,
governance-level batch claim tooling or periodic cleanup) so future maintainers
understand the persistence and scaling implications.
- Around line 586-626: The code removes the pending deposit with
PendingDeposits::take before calling EvmCaller::erc20_mint_into, relying on
frame_support::storage::with_storage_layer to roll back storage on EVM call
failure; add a concise comment in claim_pending_deposit next to the
PendingDeposits::take line documenting that the take happens prior to the mint
and that the surrounding frame_support::storage::with_storage_layer provides the
transactional rollback guarantee so future changes won't accidentally break this
invariant.

In `@pallets/moonbeam-foreign-assets/src/tests.rs`:
- Around line 494-558: Add a unit test that exercises the U256 overflow path in
EvmForeignAssets::deposit_asset: create the foreign asset and freeze it (as in
pending_deposits_accumulate), set the pending_deposits for a beneficiary to
U256::MAX (or perform a deposit of U256::MAX) and then call deposit_asset with
any positive amount (e.g. 1) and assert it returns
Err(xcm::latest::XcmError::Overflow) and that pending_deposits is unchanged;
name the test e.g. pending_deposits_overflow and reuse symbols from the existing
test (EvmForeignAssets::create_foreign_asset,
EvmForeignAssets::freeze_foreign_asset, <EvmForeignAssets as
xcm_executor::traits::TransactAsset>::deposit_asset,
EvmForeignAssets::pending_deposits).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
pallets/moonbeam-foreign-assets/src/lib.rs (2)

825-838: Pending deposit accumulation path is not wrapped in a storage transaction.

This is safe as-is because nothing can fail after the PendingDeposits::insert on line 831 (deposit_event is infallible). However, it's inconsistent with the active-mint path (line 842) and the claim path (line 616), both of which use with_storage_layer. Wrapping this block too would make the code uniformly defensive against future additions to this branch.

♻️ Suggested defensive wrapping
 			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,
-				});
+				frame_support::storage::with_storage_layer(|| {
+					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,
+					});
+
+					Ok(())
+				})?;
 			} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/lib.rs` around lines 825 - 838, The
PendingDeposit accumulation branch for AssetStatus::FrozenXcmDepositAllowed
should be wrapped in the same storage transaction helper used elsewhere to be
defensive against future fallible ops: enclose the logic that calls
PendingDeposits::<T>::get, computes total_pending, PendingDeposits::<T>::insert
and Pallet::<T>::deposit_event inside with_storage_layer(...) (the same helper
used by the active-mint and claim paths) so the read-modify-write and event
emission occur within a storage-layer transaction; locate the branch matching
AssetStatus::FrozenXcmDepositAllowed and wrap the get/checked_add/insert/event
sequence with with_storage_layer to match existing patterns.

596-606: Consider adding a debug log before discarding the original error on the EVM mint failure path.

On line 620, the original error from with_storage_layer is discarded via map_err(|_| ...). During incident triage, the underlying EVM revert reason can be valuable. A log::debug! or log::warn! before mapping would preserve this information without changing the error semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/lib.rs` around lines 596 - 606, The EVM
mint failure path currently discards the underlying error via map_err(|_| ...)
in the call to with_storage_layer; update the branch where
with_storage_layer(...).map_err(|_| ...) is used to first log the original error
(e.g. log::debug! or log::warn! with the captured `err`) before mapping it to
the existing pallet error, so change map_err(|_| ...) to map_err(|err| {
log::debug!(target: "pallet::moonbeam_foreign_assets", "EVM mint failed: {:?}",
err); /* then return the same mapped error */ Error::<T>::... }) and add the log
crate import if missing; keep the mapped error semantics unchanged and reference
the with_storage_layer call and the map_err closure in your edit.
pallets/moonbeam-foreign-assets/src/tests.rs (1)

426-824: Consider adding event assertions for PendingDepositRecorded and PendingDepositClaimed.

The existing test suite (e.g., create_foreign_and_freeze_unfreeze_using_xcm) uses expect_events to verify emitted events. The new tests verify storage state but don't assert that the expected events were emitted. Adding event checks for at least the happy paths (xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset and claim_pending_deposit_success) would increase confidence in the event-driven off-chain integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/tests.rs` around lines 426 - 824, Add
event assertions: in the test
xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset, after the deposit that
writes to PendingDeposits assert the PendingDepositRecorded event was emitted
(use expect_events or match on System::events() for PendingDepositRecorded with
asset id 1, beneficiary H160 and amount U256::from(100)); in
claim_pending_deposit_success, after calling
EvmForeignAssets::claim_pending_deposit assert PendingDepositClaimed was emitted
(with asset id 1, beneficiary H160 and amount U256::from(500)) and then assert
the PendingDeposits storage is cleared; reference the test names
xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset and
claim_pending_deposit_success and the event identifiers PendingDepositRecorded
and PendingDepositClaimed when adding these checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pallets/moonbeam-foreign-assets/src/tests.rs`:
- Around line 813-822: The test currently only checks result.is_err() for the
call to EvmForeignAssets::deposit_asset; instead assert the exact error variant
to ensure the FrozenXcmDepositForbidden path yields the expected
MatchError::AssetNotHandled (which maps to xcm::latest::Error::AssetNotFound).
Update the assertion to compare result.unwrap_err() (or pattern-match result)
against xcm::latest::Error::AssetNotFound.into() (or
MatchError::AssetNotHandled.into()) so the test fails if a different error is
returned from deposit_asset.

---

Nitpick comments:
In `@pallets/moonbeam-foreign-assets/src/lib.rs`:
- Around line 825-838: The PendingDeposit accumulation branch for
AssetStatus::FrozenXcmDepositAllowed should be wrapped in the same storage
transaction helper used elsewhere to be defensive against future fallible ops:
enclose the logic that calls PendingDeposits::<T>::get, computes total_pending,
PendingDeposits::<T>::insert and Pallet::<T>::deposit_event inside
with_storage_layer(...) (the same helper used by the active-mint and claim
paths) so the read-modify-write and event emission occur within a storage-layer
transaction; locate the branch matching AssetStatus::FrozenXcmDepositAllowed and
wrap the get/checked_add/insert/event sequence with with_storage_layer to match
existing patterns.
- Around line 596-606: The EVM mint failure path currently discards the
underlying error via map_err(|_| ...) in the call to with_storage_layer; update
the branch where with_storage_layer(...).map_err(|_| ...) is used to first log
the original error (e.g. log::debug! or log::warn! with the captured `err`)
before mapping it to the existing pallet error, so change map_err(|_| ...) to
map_err(|err| { log::debug!(target: "pallet::moonbeam_foreign_assets", "EVM mint
failed: {:?}", err); /* then return the same mapped error */ Error::<T>::... })
and add the log crate import if missing; keep the mapped error semantics
unchanged and reference the with_storage_layer call and the map_err closure in
your edit.

In `@pallets/moonbeam-foreign-assets/src/tests.rs`:
- Around line 426-824: Add event assertions: in the test
xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset, after the deposit that
writes to PendingDeposits assert the PendingDepositRecorded event was emitted
(use expect_events or match on System::events() for PendingDepositRecorded with
asset id 1, beneficiary H160 and amount U256::from(100)); in
claim_pending_deposit_success, after calling
EvmForeignAssets::claim_pending_deposit assert PendingDepositClaimed was emitted
(with asset id 1, beneficiary H160 and amount U256::from(500)) and then assert
the PendingDeposits storage is cleared; reference the test names
xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset and
claim_pending_deposit_success and the event identifiers PendingDepositRecorded
and PendingDepositClaimed when adding these checks.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 20, 2026
Copy link
Collaborator

@librelois librelois left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a new end2end dev test with a scenario covering: freeze -> deposit -> unfreeze -> permissionless claim

@manuelmauro
Copy link
Contributor Author

Please add a new end2end dev test with a scenario covering: freeze -> deposit -> unfreeze -> permissionless claim

Addressed in 1461c00

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
test/suites/dev/moonbase/test-assets/test-foreign-assets-xcm-pending-deposits.ts (1)

127-130: Prefer a typed codec accessor over BigInt(…as string) to avoid silent breakage on encoding changes.

pendingDeposit is a SCALE-decoded U256; calling .toJSON() then casting to string and wrapping in BigInt() works today (polkadot-js returns a "0x…" hex string for large values) but is fragile. A typed query or using the codec's .toBigInt() / .toString() directly is more robust.

♻️ Proposed fix
-        const pendingDeposit = (
-          await context.polkadotJs().query.evmForeignAssets.pendingDeposits(assetId, beneficiary)
-        ).toJSON();
-        expect(BigInt(pendingDeposit as string)).to.eq(depositAmount);
+        const pendingDeposit = (
+          await context.polkadotJs().query.evmForeignAssets.pendingDeposits(assetId, beneficiary)
+        );
+        expect(pendingDeposit.toString() !== "0" && BigInt(pendingDeposit.toHex())).to.eq(depositAmount);

Or, if the codec exposes .toBigInt():

-        const pendingDeposit = (
-          await context.polkadotJs().query.evmForeignAssets.pendingDeposits(assetId, beneficiary)
-        ).toJSON();
-        expect(BigInt(pendingDeposit as string)).to.eq(depositAmount);
+        const pendingDeposit = (
+          await context.polkadotJs().query.evmForeignAssets.pendingDeposits(assetId, beneficiary)
+        );
+        expect(pendingDeposit.toBigInt()).to.eq(depositAmount);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/suites/dev/moonbase/test-assets/test-foreign-assets-xcm-pending-deposits.ts`
around lines 127 - 130, The test currently calls .toJSON() on the SCALE-decoded
U256 and coerces it via BigInt(pendingDeposit as string), which is fragile;
instead obtain a typed codec or use the codec's native accessor on the returned
value from context.polkadotJs().query.evmForeignAssets.pendingDeposits(assetId,
beneficiary) (e.g. call .toBigInt() or .toString() on the codec instance) and
compare that result to depositAmount; update the pendingDeposit assignment and
the expect assertion to use the codec's .toBigInt() (or a strongly typed query
result) rather than BigInt(... as string) to avoid encoding breakage.
pallets/moonbeam-foreign-assets/src/lib.rs (1)

817-848: Add an exhaustive match after the FrozenXcmDepositForbidden early-return to guard against future AssetStatus variants.

The else branch currently silently routes any unknown future variant to the Active (EVM-mint) path. If a new AssetStatus variant is introduced, the compiler will not flag this. Preserving the early return for FrozenXcmDepositForbidden (before beneficiary resolution) maintains the current error-priority semantics.

♻️ Proposed refactor
         if let AssetStatus::FrozenXcmDepositForbidden = asset_status {
             return Err(XcmError::FailedToTransactAsset(
                 "asset is frozen and XCM deposits are forbidden",
             ));
         }

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

-        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)
-            })?;
-        }
+        match 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,
+                });
+            }
+            AssetStatus::Active => {
+                // 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)
+                })?;
+            }
+            AssetStatus::FrozenXcmDepositForbidden => {
+                unreachable!("FrozenXcmDepositForbidden handled above")
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/lib.rs` around lines 817 - 848, Replace
the current if/matches check on asset_status with an exhaustive match over
AssetStatus (after the existing early-return for
AssetStatus::FrozenXcmDepositForbidden and after resolving beneficiary) so each
known variant is handled explicitly: handle AssetStatus::FrozenXcmDepositAllowed
by computing total_pending, inserting PendingDeposits::<T>::insert(...) and
emitting Event::PendingDepositRecorded, handle the Active/expected minting
variant by performing the storage-layer
EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount) call, and
add a catch-all arm that returns a clear XcmError (or MatchError) for any
unknown future variant; reference match_asset, asset_status, PendingDeposits,
beneficiary and EvmCaller::erc20_mint_into when making these changes so the
compiler will force exhaustiveness on AssetStatus.
pallets/moonbeam-foreign-assets/src/tests.rs (1)

612-688: Consider asserting PendingDepositRecorded/PendingDepositClaimed events in the new deposit and claim tests.

claim_pending_deposit_success verifies storage state and balance but never checks that PendingDepositClaimed was emitted. Similarly, xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset and pending_deposits_accumulate don't assert PendingDepositRecorded. Adding event assertions prevents silent regressions where storage is updated correctly but events are dropped or malformed.

♻️ Example: add event assertion to `claim_pending_deposit_success` (lines 676-686)
         // Pending deposit should be cleared
         assert_eq!(
             EvmForeignAssets::pending_deposits(1, beneficiary_h160),
             None
         );

+        // Verify PendingDepositClaimed event was emitted
+        expect_events(vec![
+            Event::PendingDepositClaimed {
+                asset_id: 1,
+                beneficiary: beneficiary_h160,
+                amount: U256::from(500),
+            }
+        ]);
+
         // Verify beneficiary received the tokens

Also applies to: 426-558

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pallets/moonbeam-foreign-assets/src/tests.rs` around lines 612 - 688, The
test claim_pending_deposit_success should also assert the emitted events to
catch regressions: after the deposit call (TransactAsset::deposit_asset) assert
that PendingDepositRecorded (from the EvmForeignAssets pallet events) was
emitted with the expected asset id, beneficiary and amount, and after
claim_pending_deposit assert that PendingDepositClaimed was emitted with the
expected asset id, beneficiary and amount; add similar PendingDepositRecorded
assertions to xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset and
pending_deposits_accumulate tests to ensure events are emitted alongside the
storage and balance checks (look for EvmForeignAssets::pending_deposits,
claim_pending_deposit, and the pallet event variants
PendingDepositRecorded/PendingDepositClaimed to locate the right spots).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pallets/moonbeam-foreign-assets/src/tests.rs`:
- Around line 813-824: Test assertion is correct and should remain as-is: ensure
the call to <EvmForeignAssets as
xcm_executor::traits::TransactAsset>::deposit_asset(...) continues to be
asserted against Err(XcmError::FailedToTransactAsset("asset is frozen and XCM
deposits are forbidden")) so it matches the updated deposit_asset implementation
and not the old AssetNotFound/MatchError path.

---

Nitpick comments:
In `@pallets/moonbeam-foreign-assets/src/lib.rs`:
- Around line 817-848: Replace the current if/matches check on asset_status with
an exhaustive match over AssetStatus (after the existing early-return for
AssetStatus::FrozenXcmDepositForbidden and after resolving beneficiary) so each
known variant is handled explicitly: handle AssetStatus::FrozenXcmDepositAllowed
by computing total_pending, inserting PendingDeposits::<T>::insert(...) and
emitting Event::PendingDepositRecorded, handle the Active/expected minting
variant by performing the storage-layer
EvmCaller::<T>::erc20_mint_into(contract_address, beneficiary, amount) call, and
add a catch-all arm that returns a clear XcmError (or MatchError) for any
unknown future variant; reference match_asset, asset_status, PendingDeposits,
beneficiary and EvmCaller::erc20_mint_into when making these changes so the
compiler will force exhaustiveness on AssetStatus.

In `@pallets/moonbeam-foreign-assets/src/tests.rs`:
- Around line 612-688: The test claim_pending_deposit_success should also assert
the emitted events to catch regressions: after the deposit call
(TransactAsset::deposit_asset) assert that PendingDepositRecorded (from the
EvmForeignAssets pallet events) was emitted with the expected asset id,
beneficiary and amount, and after claim_pending_deposit assert that
PendingDepositClaimed was emitted with the expected asset id, beneficiary and
amount; add similar PendingDepositRecorded assertions to
xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset and
pending_deposits_accumulate tests to ensure events are emitted alongside the
storage and balance checks (look for EvmForeignAssets::pending_deposits,
claim_pending_deposit, and the pallet event variants
PendingDepositRecorded/PendingDepositClaimed to locate the right spots).

In
`@test/suites/dev/moonbase/test-assets/test-foreign-assets-xcm-pending-deposits.ts`:
- Around line 127-130: The test currently calls .toJSON() on the SCALE-decoded
U256 and coerces it via BigInt(pendingDeposit as string), which is fragile;
instead obtain a typed codec or use the codec's native accessor on the returned
value from context.polkadotJs().query.evmForeignAssets.pendingDeposits(assetId,
beneficiary) (e.g. call .toBigInt() or .toString() on the codec instance) and
compare that result to depositAmount; update the pendingDeposit assignment and
the expect assertion to use the codec's .toBigInt() (or a strongly typed query
result) rather than BigInt(... as string) to avoid encoding breakage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-review Trigger CodeRabbit Agent review B0-silent Changes should not be mentioned in any release notes D5-nicetohaveaudit⚠️ PR contains trivial changes to logic that should be properly reviewed. not-breaking Does not need to be mentioned in breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants