As such, the code as you have written works, but let me explain further why I initially felt that the entire process is flawed – it’s due to the fact that chain A now needs to trust chain B for safeguarding its sovereign account. The entire idea behind reserve-backed asset transfers is that A and B don’t necessarily trust each other but both trust chain R, and hence they can communicate via R. This additional step of transferring NFTs to the sovereign account of A on chain B now re-establishes the requirement of trust between A and B.
Thank you for highlighting this for me! I totally agree with this.
Considering this and several other issues that came to me, I had to rework the solution.
There are issues with using the XCM Transact as the utility to transfer metadata. The limit of 100 kb is not limiting a single XCM message but the entire HRMP channel. So if there are many cross-chain NFT transfers in a short time, many of these transfers will be rejected since they won’t fit into the channel. Thus, we have to optimize the metadata transfer. Since we don’t have the means to transfer big chunks of data via XCM, we should use an on-chain-verifiable but off-chain method.
The main idea of the new approach is to use the SetTopic XCM command to transfer the short, fixed-length hash of the metadata alongside an NFT. Accordingly, the NonFungibleAdapter should react to the set topic. The metadata will be provided off-chain via the Preimages pallet. The metadata hash represents the valid metadata so the target chain can check the correctness of metadata provided off-chain. This approach demands a user to take more than one action to perform the cross-chain transfer, but in return, the method is more straightforward, which means it is less error-prone.
Why is the metadata hash sufficient? The previously discussed method struggles to send authentic NFT metadata from the sender chain. It tries to send the actual data through XCM, but we only care that the target chain receives the metadata identical to the last metadata state on the sender chain. There is no need to make the Reserve Chain hold the metadata for this purpose; we only need the sender chain to send the metadata hash representing the correct metadata. Since the sender chain is the owner of the NFT (and the Reserve Chain testifies to it), we trust this hash.
The other thing to mention is the metadata format. While different chains have different metadata formats, it would be good to have some standard formats to communicate between chains. There could be several standards, so we have to provide some metadata container for transfers describing:
- The standard used
- Maybe some information about the standard
- The payload itself – the format defined by the standard, but one way or the other, it will be some key-value pair set
The metadata hash should be computed based on this metadata container – the hash will represent not only the payload but the standard of the metadata to maintain metadata format flexibility.
There are too many things to describe to explain the entire implementation, despite the more straightforward nature of the new solution. So let’s define the new approach without complicating details to avoid losing the point.
Cross-chain NFT transfer flow (the happy path)
From a user perspective, five actions should be performed by them to transfer an NFT:
Preparing the NFT for transferring – an extrinsic call
Building and executing the XCM program – an extrinsic call of some pallet
Acquiring the metadata from the source chain via the Runtime API
Noting the metadata Preimage on the target chain
Binding the metadata to the NFT – an extrinsic call that will interpret the metadata blob from the Preimage pallet and store the metadata in the native representation
The Sender wants to send an NFT to the Beneficiary
- The Sender calls the “prepare-for-xc-transfer” extrinsic. This extrinsic will:
- Compute the metadata hash of the NFT. We can’t do it during the transfer because this operation could be unpredictably costly – remember, it computes the hash of the metadata container, so the chain should translate the metadata from its internal representation to the standard one and then compute the hash (during the sending of the XCM message, the chain will take this hash and pass it to the SetTopic command)
- Lock the NFT for the non-XCM operations – since we computed and stored the hash, the NFT shouldn’t be modified
- Remember the current owner of the NFT. This is needed for error handling (will be described later)
The Sender sends the NFT with the help of a special “xcm-nft-transfer” pallet (actually, it could be the “xtokens” pallet). The pallet executes the usual
InitiateReserveWithdraw XCM command and supplies the hash of the current metadata along with other XCM commands. The pallet utilizes the
SetTopic XCM command for this purpose
The target chain receives the XCM commands, and the NonFungibleAdapter should take into account the current XCM context (the following could be abstracted in Substrate):
- It should read the topic – it’s the hash of the metadata
Request the preimage
- Check if the metadata preimage exists
- [maybe] If no such data is provided, emit an event informing the user (or some other interested party) that the metadata should be provided to make the NFT usable on the target chain
- Accept the NFT but keep it locked for non-XCM operations since we can’t bind the metadata yet because it’s hard to predict how much weight it would take (we don’t know the weight of the metadata blob interpretation and subsequent storing it in the target chain’s NFT storage)
Someone (this operation can be done by anyone) acquires the current metadata by reading it on the source chain via the Runtime API
Someone notes the preimage on the target chain with the acquired metadata. The Preimages pallet generates a hash for the metadata. (Note that if the hash of the provided data is correct, this operation will be free after the transfer because the target chain requested this preimage. See the Preimage pallet code: substrate/lib.rs at 897b95d98424a44eb493eb982e5ac88d37f60cb5 · paritytech/substrate · GitHub)
Once the NFT is deposited and the metadata preimage is noted, Someone (this operation is a common good) can bind the metadata to the NFT via an extrinsic. This extrinsic will try to interpret the metadata blob, translate it into the chain’s internal representation, and store it as native metadata. In case of success, the extrinsic will unrequest the metadata blob provided by its hash, hence removing the metadata preimage. Also, it will unlock the NFT for non-XCM operations.
Relaxed NFT assets limit per cross-chain transfer
We can transfer a lot of NFTs without worrying if we can fit them into the HRMP channel since we are transferring only the NFT ids and the 32-byte metadata hashes.
Addressing the cross-chain metadata compatibility issue
Let’s describe the core issue we are considering in this analysis, which could occur when using the Reserve Chain concept and show how the alternative approach proposed above would solve it.
Each parachain that handles NFTs has its own set of rules for metadata. These rules dictate which keys are available and which of them can or cannot be updated. If an NFT is transferred with incompatible metadata, it will not be possible to bind it to the NFT on the target chain. However, this is a logical limitation, not a physical one. Even if the NFT metadata needs to be adjusted before or after the transfer, the owner may still want to transfer the NFT nonetheless.
- The adjustment of the NFT could be made manually by the NFT owner before the transfer – the target chain could provide a Runtime API allowing the NFT owner to perform a compatibility check prior to executing a transfer.
- The adjustment of the NFT could be made automatically by the target chain when binding the metadata. But the owner’s explicit consent is required to perform the metadata coercion.
In either scenario mentioned above, the metadata may be lost or altered - a lossy conversion. As a result, the NFT owner should be aware of what will happen and either make the adjustments manually or give explicit consent for the adjustments to be made.
It is not a big problem considering the 2-party transfer: Reserve Chain → Derivative Chain, and vice versa.
But, if we consider the 3-party transfers (Chain A → Reserve Chain → Chain B), it becomes obvious that we could easily induce unnecessary data loss during the transmission.
Chain A → (possible data loss) → Reserve Chain → (possible data loss) → Chain B.
But here is the question: if the Reserve Chain has no other role but to safeguard the NFT (meaning, the NFT will not participate in the Reserve Chain’s logic and hence has no obligation to comply with its rules regarding metadata), why is an adjustment step that could cause data loss being considered in the above procedure at all?
A way to sidestep this problem entirely is: the Reserve Chain shouldn’t hold the metadata for these transfers! The only thing that it must do is to transfer the NFT between sovereign accounts and send the metadata hash to the target chain. Since the metadata hash was provided by Chain A while it held the NFT under its sovereign account, this hash represents the final state of the NFT provided by its owner – Chain A. There is no need to store the metadata anywhere since it is already stored in the previous blocks on Chain A. The hash guarantees that only the correct metadata will be bound to the NFT, so we don’t have to trust Chain A anymore.
Actually, the origin of the metadata becomes irrelevant as the integrity of the NFT is maintained by the hash.
Note: it is not mandatory that two different parachains have different metadata rules. They may be compatible, but we must describe the general case here.
The NFT metadata format is flexible
Using an abstract metadata container for the NFT transferring doesn’t limit the chains’ understanding of the metadata. Several different standards (and possibly different versions of them) could be defined in the future.
Not only parachains
Since this approach uses the standard XCM operations only to ensure the authenticity of the metadata while at the same time the metadata itself is transferred off-chain – NFTs can be transferred not only between parachains but also to/from the external consensus systems like Ethereum via the corresponding bridge.
We should avoid Governance involvement as much as possible when something goes wrong. For example, if the DepositAsset XCM command fails, the error reporting should automatically trigger the NFT rescue/recovery procedure. Recall that each parachain decides on its own how to handle foreign NFTs. It can also reject an NFT, so we must ensure that the NFT can never be lost.
The target chain should notify the sender chain (either through the Reserve Chain if it is a 3-party transfer or directly if it is a 2-party transfer) when an XCM error occurs. We can do that using the ReportHolding XCM command in the XCM Error Handler. Upon receiving the
Response::Assets message (with the dedicated query id for error handling of the XC-NFT transfers), the sender chain will need to restore all the NFTs mentioned in the message to the original owners and request the metadata hashes for future metadata binding (which means the chain needs to keep track of the original owner and the corresponding hash during the cross-chain transfer preparation stage).
Restoring the NFT metadata is identical to its transferring – a user should acquire the metadata from the previous blocks via Runtime API and then bind it to the NFT.
However, some errors (e.g., when insufficient funds were supplied to BuyExecution, resulting in an Asset Trap scenario) cannot be handled without Governance. But such errors would need to be caught during testing.
If the error message goes through the Reserve Chain (a 3-party transfer), the Reserve Chain should transfer the NFTs back to the sender chain’s sovereign account and send a similar error message to the sender chain so it can restore the NFTs on its side.
Worthy of mention
- The same algorithm should compute the metadata hash on both sides. But the Preimage pallet’s hashing algorithm could differ on the target chain. If so, the second instance of the Preimage pallet with the correct hashing algorithm should be used on the target chain.
- Recall the problem of funding the sovereign accounts from the previous solution. Transferring NFTs is not so tricky with the new approach and perfectly fits the XCM design. Note that when parachains open HRMP channels, they usually have means to transfer their native tokens between them. With the new approach, we can easily transfer native tokens to buy execution on the target chain alongside NFTs.
This solution can be easily integrated into the current infrastructure.
- The standard NFT interface (from “frame_support”) should be able to lock NFTs for non-XCM operations.
- The NonFungibleAdapter should use the extended NFT interface to lock the NFT for non-XCM operations. Also, the adapter should consider the XCM context.
- The “xtokens” pallet can be extended to work with NFTs and their metadata (it should compose the correct XCM programs and respond to the errors/successes accordingly).
- Currently, the Polkadot XCM pallet can process only one query response per query id. Therefore, it would be good to preallocate a couple of query ids for multiple error/success handling.
Here you can find the sequence diagrams and XCM programs’ pseudocode: XC-NFT transfer + preimages · GitHub.
I would be happy to improve this solution if I missed something.
Moreover, if this solution seems viable, the Unique Network team is ready to implement all the necessary parts to give the ecosystem the means for cross-chain NFT transferring.