I am currently working on the transfer of Moonbeam ERC20 tokens through XCM, and in this context I have read all the resources I could find on XCM to make sure that my implement is compliant with the XCM standards and expectations, but I can’t find a not-convoluted way.
The idea of this post is not to talk about ERC20 in particular, nor about Moonbeam, but to answer the more general question: how to manage properly through XCM an asset which is not burnable by the XCVM?".
Following the reading of xcm format, these kind of assets should never be placed in the Holding Register as they still exist in the on-chain persistent storage (on our example, in the contract storage).
That’s why I went with an implementation of the AssetTransactor that returns the error XcmError::NotWithdrawable
for method withdraw_asset
.
To send a not-withdrawable asset to another chain it works, because the TransferReserveAsset
instruction does not need to go through the Holding, but the problem arises to send back this asset to the reserve chain, because the InitiateReserveWithdraw
instruction forces to send a message like [WithdrawAsset, .., DepositAsset]
.
A reserve asset never needs to be burned and then minted, it just needs to be transferred from/to the sovereign account, which is why I think there should be a way to properly manage these kinds of assets.
Proposals
Solution 1: Morph incoming messages (my current workaround)
Create a specific Barrier that morph incoming messages that match the exact pattern [WithdrawAsset, ClearOrigin, BuyExecution, DepositAsset]
into [WithdrawAsset, BuyExecution, TransferAsset, DepositAsset]
.
Of course, only the not-withdrawable assets should be moved to TransferAsset, the withdrawable one should still in the initial WithdrawAsset instruction to be able to pay fees.
It seems to work well but it’s convoluted, and limited to specific cases.
Solution 2: Rework instruction InitiateReserveWithdraw
to handle properly not-withdrawable assets
Ideally, the chain that runs InitiateReserveWithdraw
doesn’t have to worry about whether the asset is withdrawable or not on the reserve chain, it should just have to say what it wants to do with this asset (deposit on a recipient account or manipulate it for other things).
We could for example add the parameter assets_to_transfer: Option<(MultiAssetFilter, Multilocation)>
to be able to express which assets just need to be transferred, the new implementation of InitiateReserveWithdraw
instruction could look something like this:
InitiateReserveWithdraw { assets, assets_to_transfer, reserve, xcm } => {
let assets = Self::reanchored(
self.holding.saturating_take(assets),
&reserve,
Some(&mut self.holding),
);
let mut message = if let Some((filter, beneficiary)) = assets_to_transfer {
vec![WithdrawAsset(assets), TransferAsset {
assets: assets.filter(|asset| filter.matches(asset)).collect(),
beneficiary
}, ClearOrigin]
} else {
vec![WithdrawAsset(assets), ClearOrigin]
};
message.extend(xcm.0.into_iter());
self.send(reserve, Xcm(message), FeeReason::InitiateReserveWithdraw)?;
Ok(())
},
For backward compatibility, we can just set the new parameter to None when we convert the instruction from v3 to v4.
In the case of a user/dapp that just wants to send 2 assets (with one to pay the fees), it would submit the following message:
Xcm(vec![
WithdrawAsset(assets.clone()),
InitiateReserveWithdraw {
assets: All.into(),
assets_to_transfer: Option(Definite(assets_to_transfer), beneficiary),
reserve: reserve.clone(),
xcm: Xcm(vec![
BuyExecution { fees, weight_limit },
DepositAsset { assets: All.into(), beneficiary }
]),
},
])
Solution 3: Create a new dedicated instruction
For example, we could create a new instruction TransferToReserve
with the following parameters:
TransferToReserve {
assets: MultiAssetsFilter,
fees: Option<(MultiAsset, WeightLimit)>,
reserve:: Multilocation
beneficiary: Multilocation
xcm: Xcm,
}
This new instruction would send a message like (just pseudo-code):
let mut message = if let Some((fees, weight_limit)) = fees {
vec![WithdrawAsset(fees.into()), BuyExecution { .. },
TransferAsset {
assets: assets.filter(|asset| !filter.matches(&fees)).collect(),
beneficiary
}, ClearOrigin]
} else {
vec![TransferAsset(assets), ClearOrigin]
};
What bothers me with this solution is that every user/dapp dev has to ask himself if he should use the InitiateReserveWithdraw
or TransferToReserve
instruction, which makes understanding and using XCM more complex. I think it is better to modify the existing instruction rather than adding a new one.
Of course all this are only proposals, maybe there is a simpler solution that I have not seen, if so I would be happy to learn it