Claiming Trapped Assets¶
Introduction¶
When XCM execution fails or succeeds, leftover assets can become "trapped" on the destination chain. These assets are held by the system but are not accessible through normal means. XCM provides mechanisms to claim these trapped assets and recover them. This guide details the process and required steps to claim trapped assets.
Trapped Assets Causes¶
Assets become trapped whenever execution halts and there are leftover assets. This can happen for example if:
-
An XCM execution throws an error in any instruction when assets are in holding such as:
DepositAsset
can't deposit because the account doesn't exist.Transact
can't execute the call because it doesn't exist.PayFees
not enough funds or not paying enough for execution.
-
XCM execution finishes successfully but not all assets were deposited:
- Funds were withdrawn but some were not deposited.
Transact
overestimated the weight andRefundSurplus
got some funds into holding that were never deposited- Fees in
PayFees
were overestimated and some were kept there until the end
The ClaimAsset
Instruction¶
The ClaimAsset
instruction allows retrieving assets trapped on a chain:
XcmV5Instruction.ClaimAsset({
assets: /* Exact assets to claim, these must match those in the `AssetsTrapped` event */,
ticket: /* Additional information about the trapped assets, e.g. the XCM version that was in use at the time */
});
Parameters¶
-
assets
: The trapped assets that want to be claimed. These must be exactly the same as the ones that appear in theAssetsTrapped
event. -
ticket
: Additional information about the trapped assets. Currently only specifies the XCM version used when the assets got trapped. Must be of the form:Ensure to replace the
INSERT_XCM_VERSION_HERE
with the actual XCM version used when the assets got trapped.
Basic Claiming Process¶
When assets are trapped you'll see the AssetsTrapped
event:
To claim these assets, a message like the following needs to be sent from the origin:
const claimAssetsXcm = XcmVersionedXcm.V5([
// Claim trapped DOT.
XcmV5Instruction.ClaimAsset({
assets: [{
// USDC.
id: {
parents: 0,
interior: XcmV5Junctions.X2([
XcmV5Junction.PalletInstance(50),
XcmV5Junction.GeneralIndex(1337n),
]),
},
fun: XcmV3MultiassetFungibility.Fungible(49_334n) // 0.049334 units.
}],
// Version 5.
ticket: { parents: 0, interior: XcmV5Junctions.X1(XcmV5Junction.GeneralIndex(5n)) }
}),
XcmV5Instruction.PayFees(/* Pay for fees */),
XcmV5Instruction.DepositAsset(/* Deposit everything to an account */),
]);
Note that this example uses the claimed USDC assets to pay for the execution fees of the claiming message. If the trapped asset cannot be used for fee payment on the destination chain, you need a different approach: first WithdrawAsset
(with fee-eligible assets), then PayFees
, then ClaimAsset
, and finally DepositAsset
.
In this case, the origin is a local account so the execute()
transaction needs to be submitted by that same account. The origin could be another chain, in which case the governance of that chain would need to get involved, or an account on another chain, in which case the execute()
transaction would need to be submitted on that other chain and a message sent to the chain with trapped funds.
Multiple assets can be claimed with the same message. This is useful when governance needs to get involved.
const claimAssetsXcm = XcmVersionedXcm.V5([
// Claim trapped DOT.
XcmV5Instruction.ClaimAsset(/* ... */),
XcmV5Instruction.PayFees(/* Pay for fees */),
XcmV5Instruction.ClaimAsset(/* ... */),
XcmV5Instruction.ClaimAsset(/* ... */),
XcmV5Instruction.ClaimAsset(/* ... */),
XcmV5Instruction.DepositAsset(/* Deposit everything to an account */),
]);
The AssetClaimer
Hint¶
The AssetClaimer
execution hint allows setting a specific location that can claim trapped assets, making the claiming process easier. This is set after withdrawing assets and before anything else:
const failingXcm = XcmVersionedXcm.V5([
// Withdraw 1 DOT (10 decimals).
XcmV5Instruction.WithdrawAsset([
{
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(10_000_000_000n),
},
]),
// Set the asset claimer.
XcmV5Instruction.SetHints({
hints: [
Enum(
'AssetClaimer',
{
location: {
parents: 0,
interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
id: FixedSizeBinary.fromAccountId32(SS58_ACCOUNT),
network: undefined,
})),
}
}
)
]
}),
// Pay fees.
XcmV5Instruction.PayFees({
asset: {
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(1_000_000_000n),
},
}),
// Explicitly trap. Alternatively, doing nothing would still result in the assets getting trapped.
XcmV5Instruction.Trap(0n),
]);
This allows this other SS58_ACCOUNT
to claim the trapped assets. This could also be done before a transfer.
Teleport with custom asset claimer example
const setAssetClaimerRemotely = XcmVersionedXcm.V5([
// Withdraw 1 DOT (10 decimals).
XcmV5Instruction.WithdrawAsset([
{
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(10_000_000_000n),
},
]),
// Pay fees.
XcmV5Instruction.PayFees({
asset: {
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(1_000_000_000n),
},
}),
// Cross-chain transfer.
XcmV5Instruction.InitiateTransfer({
destination: { parents: 1, interior: XcmV5Junctions.X1(XcmV5Junction.Parachain(1000)) },
remote_fees: Enum(
'Teleport',
XcmV5AssetFilter.Definite([
{
id: { parents: 1, interior: XcmV5Junctions.Here() },
fun: XcmV3MultiassetFungibility.Fungible(1_000_000_000n),
},
])
),
preserve_origin: false,
assets: [
Enum(
'Teleport',
XcmV5AssetFilter.Wild(XcmV5WildAsset.AllCounted(1))
),
],
remote_xcm: [
// Set the asset claimer on the destination chain.
// If any asset gets trapped, this account will be able to claim them.
XcmV5Instruction.SetHints({
hints: [
Enum(
'AssetClaimer',
{
location: {
parents: 0,
interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
id: FixedSizeBinary.fromAccountId32(SS58_ACCOUNT),
network: undefined,
})),
}
}
)
]
}),
XcmV5Instruction.DepositAsset({
assets: XcmV5AssetFilter.Wild(XcmV5WildAsset.AllCounted(1)),
beneficiary: {
parents: 1, interior: XcmV5Junctions.X1(XcmV5Junction.AccountId32({
id: FixedSizeBinary.fromAccountId32(SS58_ACCOUNT),
network: undefined,
})),
}
}),
],
}),
]);
Best practices¶
- Always set a claimer: Include
SetAssetClaimer
in XCMs with valuable assets. - Use accessible locations: Ensure the claimer location is controlled by someone who can act.
- Monitor for failures: Track XCM execution to detect when claiming is needed.
- Test claiming flows: Verify your claiming logic works in test environments.
- Document recovery procedures: Maintain clear instructions for asset recovery.
Setting a custom asset claimer is a good practice for recovering trapped assets without the need for governance intervention.
| Created: August 14, 2025