Parity Tech mandated Quarkslab to audit XCM version 2 (XCMv2), a cross consensus communication mechanism. This messaging protocol is a cornerstone of the Polkadot ecosystem as it enables communications between chains on a network. This blog post summarizes few security aspects related to this technology and its implementation. The full audit report is available in PDF format at the end of this article.
Parity Tech is actively working on developments related to the Polkadot ecosystem. Polkadot recently launched the crowdloan process as part of the long-planned objective of creating a network of blockchains interconnected via a relay chain to perform cross-chain exchanges.
In this context, Parity is working on XCM (Cross-Consensus Messaging) which provides a common format enabling Substrate-based blockchains to communicate with the relay chain and between each other. This component from the original Polkadot design is essential for Polkadot's multichain network. It enables both fungible and non-fungible asset transfers and also remote extrinsic calls.
Bridges and cross-chain technologies, in general, will play an essential role in the upcoming months or years to interconnect blockchains that are not necessarily working with the same technology or consensus rules. Among these are Hop for EVM blockchains, Interlay to bridge Bitcoin to Polkadot or Axelar Network, which aims at bridging multiple blockchain technologies.
XCM follows a similar goal and aims at bridging any kind of consensus in the future. Polkadot's founder Gavin Wood provides insights of XCM goals as "a language communicating ideas between consensus systems" [1].
Quarkslab conducted an audit of XCMv2 before parachains obtained a slot on the Polkadot relay chain and thus before the activation of the support in their blockchain. An additional security audit had already been performed by another security company.
The audit aimed at finding any cross-chain-related security issues, like incorrect lock/unlock or burn/mint on both chains, or any fairness issues between chains. This also includes logical bugs, denial-of-service and any misconfiguration (of default settings) that can have a security impact. The audit did not reveal any meaningful security-related issues.
This blog post aims at providing a glimpse of the internal working of XCM transactions and more especially the VM-based design for processing messages. It also highlights the key security and sanity checks to be performed before activating XCM on a parachain.
The XCM design and the two main use-cases — reserve transfer assets and teleport assets — have thoroughly been described by Gavin Wood in a blog post trilogy [1], [2], [3] and a workshop by Shawn Tabrizi [7]. Moonbeam also provides multiple educational contents about XCM and their usage with XC-20 tokens [4], [5].
While user applications of XCM (assets transfers, etc.) have been well described, let us focus on what's happening under the hood when transferring assets between two chains. The core component is the XCM virtual machine (XCVM). Indeed, messages exchanged are scale-encoded [6] instruction opcodes. A message is essentially a list of instructions that perform various actions like withdrawing assets from an account, depositing them, initiating a transfer, etc.
When receiving a new message, a new VM is instantiated for the lifetime of the message execution. Some instructions update registers of the VM. These registers hold the origin (identity performing the action), holding (assets manipulated), and a trader handling weight costs, surplus, and refunded amounts. The VM also contains an instruction pointer addressing the current message instruction to execute. Similarly, it contains registers pointing to error handlers and appendix handlers.
Let's consider a teleport asset transfer from the relay chain to a parachain. In this scenario, the user withdraws funds from its local account and transfers them to the XCVM, which will eventually burn the asset. Then, the chain has to transmit a message to the remote chain to mint and deposit the equivalent amount of assets into the user's account on the parachain. The complete initial message is the following:
Xcm(vec![ WithdrawAsset(assets), InitiateTeleport { assets: Wild(All), dest, Xcm(vec![ BuyExecution { fees, weight_limit }, DepositAsset { assets: Wild(All), max_assets, beneficiary }, ]); } ]);
The following animation shows the broad steps performed by the XCM pallet teleport_assets call, its processing by the XCM executor and its underlying XCVM, up to the final deposit on the remote account (here Alice).
Note that in this scenario, the destination chains have to accept the originating chain as a teleporter, i.e., an origin from which the receiver can trust that the sender rightfully destroyed assets before teleporting them, in its own XCM configuration. Otherwise, the execution of ReceiveTeleportedAsset on the destination chain will reject the message as the origin register will be untrusted. That is one of the reasons why properly configuring XCM is a critical task. Let's discuss a few aspects of the XCM configuration.
Parachain developers do not necessarily need to understand the deep intricacies of the XCM executor, but the pallet configuration leaves many levers of configuration that should be handled with care. Indeed, some configuration aspects are interdependent as highlighted in the report. Here is a quick memo of things to verify before activating XCM or accepting exchanging messages with a chain.
In the following list, elements of the XCM pallet and the XCM executor configuration are mixed, in fact, the XCM executor configuration is nested as a configuration element of the XCM pallet. They are marked with the "pallet" or the "executor" (or "both") annotation.
Three filters, XcmExecuteFilter, XcmTeleportFilter and XcmReserveTransferFilter are the first configuration types to look at. These filters are an assertion mechanism respectively for the execute, teleport and reserve_transfer extrinsics (and their limit additions). These types implement the Rust trait (or interface) Contains<T> that just requires a simple method returning true if this "contains" the input value. Two basic implementations, Everything and Nothing, return respectively true and false, and were previously called AllowAll and DenyAll. These filters are a simple way to enable or disable the core features of the XCM pallet, which are the most useful extrinsics, to execute or send messages via privileged execution allowed by the teleport and reserve_transfer extrinsics.
ExecuteXcmOrigin and SendXcmOrigin ensure which origin can respectively perform the execute and send extrinsics. They must implement the EnsureOrigin<OuterOrigin> trait. Typically, in the Polkadot or Kusama runtimes, a LocalOriginToLocation tuple composed of many converters transforms Origin into a MultiLocation and thus allows the origin or not. For example, the SignedToAccountId32 implementation transforms a signed origin into the MultiLocation corresponding to the specific account on the chain. Note that execute has a filter, and an XCM origin converter.
One of the settings to look at and verify in the executor is the Barrier. This is effectively the mechanism for the executor to filter messages in terms of payment for the execution. Complex logic can be implemented to allow free execution for certain kinds of messages and require payment for others. The Barrier type is usually a tuple of multiple structures that implements the ShouldExecute trait; each structure of the tuple implementation is called in order until some returns Ok() or all failed (see the tuple implementation). In the xcm/xcm-builder/src/barriers.rs file, many generic implementations can be found that can be combined to apply a custom pricing policy. More information can be found in the report, as the default configuration of barriers for the parachain template was audited.
The IsTeleporter and IsReserve settings are crucial for the XCM executor to know which origins can be trusted to accept teleports and reserve transfers from. They implement the FilterAssetLocation trait and can be amalgamated into tuples. They are used in the ReceiveTeleportedAsset and ReserveAssetDeposited instruction execution code and are used as an assertion just before the ex nihilo creation of assets on the destination system. This creation process is why you have to make sure you trust the sender to have reserved or burnt the assets on the other side, i.e. properly executing the original XCM instructions.
Weighers are used both in the XCM pallet and in the XCM executor configuration. For example, they will be used to weigh a newly set error or appendix handler in the executor or to adjust the weight of a call to the teleport or reserve extrinsic in the XCM pallet. They implement the WeightBounds<Call> trait. A badly implemented weigher could provide free or cheap execution that could be exploited via XCM messages. The weigher implementation in the XCM builder file is straightforward for most of the instructions, it adds a constant base weight by instruction, only Transact, SetErrorHandler and SetAppendix are a bit more sophisticated because they themselves include nested instructions or dispatchable calls.
One last thing to check is the sender, or router. It must implement the SendXcm trait, which is basically the technical implementation method that takes the destination and the XCM message. Multiple senders might be combined by creating a tuple. In the Polkadot runtime, at the time of the audit, the only sender was ChildParachainRouter that sent the message to a parachain by depositing it into the downward message passing (DMP) queue of the appropriate parachain.
The audit was carried out by two security engineers for a duration of 50 days. It was performed respectively on Polkadot runtime v0.9.13 and Cumulus v6.0.0. It did not uncover important security issues. Yet, the report discusses and describes the underlying security mechanisms of multiple XCM components. It can thus be interesting for anyone to dig into XCM inner workings.
The full audit report can be found here:
Configuring XCM should be done in a very preserving manner by denying all by default. Then, only in a discretionary way, chains should enable some messages to pass through. Most of the XCM asset transfer security boils down to deciding who is trusted as a reserve or as a teleport origin. This needs to be done with care because, when receiving assets, there are currently no mechanisms to ensure they have been properly locked/burnt on the originating chain. In this setting, the trust is shared and both chains assume the other one to behave well.
Cross-chain communication protocols are developing fast, and most of them are in their infancy in terms of usage and potential use-cases. In Polkadot, XCMv2 is already deployed on the relay chain. Furthermore, the next version, XCMv3 [8], is already under development to bring new features and improve the current version.
We would like to thank Parity for making this assessment possible and for their responsiveness during the audit.
[1] | (1, 2) Gavin Wood. XCM: The Cross-Consensus Message Format. Sept. 6, 2021 https://medium.com/polkadot-network/xcm-the-cross-consensus-message-format-3b77b1373392 |
[2] | Gavin Wood. XCM Part II: Versioning and Compatibility. Sept. 15, 2021 https://medium.com/polkadot-network/xcm-part-ii-versioning-and-compatibility-b313fc257b83 |
[3] | Gavin Wood. XCM Part III: Execution and Error Management. Sept. 29, 2021 https://medium.com/polkadot-network/xcm-part-iii-execution-and-error-management-ceb8155dd166 |
[4] | Moonbeam Docs - Cross-Consensus Messaging (XCM) https://docs.moonbeam.network/builders/xcm/overview/ |
[5] | Moonbuilders Workshop: A Technical Introduction to XCM on Moonbeam https://www.youtube.com/watch?v=5HD5rFBqvQ4 |
[6] | SCALE Codec - 3.0 https://docs.substrate.io/v3/advanced/scale-codec/ |
[7] | Sub0 Online: Getting Started with XCM - Your First Cross Chain Messages, Oct 13, 2021 https://www.youtube.com/watch?v=5cgq5jOZx9g |
[8] | XCM v3 Github Pull Request #4097 https://github.com/paritytech/polkadot/pull/4097 |