Transact
XCM contains an instruction that allows for the execution of calls (from a RuntimeCall
in a
FRAME-based system, to a smart contract function call in an EVM-based system) in a consensus system.
It is the Transact
instruction and it looks like this:
Transact {
origin_kind: OriginKind,
require_weight_at_most: Weight,
call: DoubleEncoded<Call>
}
The Transact instruction has three fields. The origin_kind
is of type
OriginKind and specifies
how the origin of the call should be interpreted. In the xcm-executor, the origin_kind
is used to
determine how to convert a MultiLocation
origin into a RuntimeOrigin
. For more information,
check out the xcm-executor config docs.
The require_weight_at_most
field tells the XCVM executing the call how much
weight it can use. If the call uses more weight than the
specified require_weight_at_most
, the execution of the call fails.
The call
field is of type DoubleEncoded<Call>
.
pub struct DoubleEncoded<T> {
encoded: Vec<u8>,
#[codec(skip)]
decoded: Option<T>,
}
XCM is consensus system agnostic; it does not know what is being encoded in the call field. Hence,
the field is a byte vector that can be freely interpreted in whatever form possible. However, the
XCVM does not inherently know how to interpret this call field nor how to decode it; it is reliant
on the T
type parameter to specify the proper codec for the byte vector. Instead of just using a
Vec<u8>
we use DoubleEncoded
as a wrapper around a pre-encoded call (Vec<u8>
) with extra
functionalities such as caching of the decoded value. We like to emphasize that the call in the
Transact
instruction can be anything from a RuntimeCall
in a FRAME-based system, to a smart
contract function call in an EVM-based system.
Each XCVM has a Transact Status Register, to record the execution result of the call that is
dispatched by the Transact
instruction. Important note: The execution of the XCM instruction
does not error when the dispatched call errors.
The status is described by the MaybeErrorCode
enum, and can either be a Success, Error or
TruncatedError if the length of the error exceeds the MaxDispatchErrorLen. For pallet-based calls,
the Error is represented as the scale encoded Error
enum of the called pallet.
ExpectTransactStatus(MaybeErrorCode)
pub enum MaybeErrorCode {
Success,
Error(BoundedVec<u8, MaxDispatchErrorLen>),
TruncatedError(BoundedVec<u8, MaxDispatchErrorLen>),
}
XCM Executor
In this section, we quickly look at how the XCM executor executes the Transact
instruction.
It executes, among other things, the following steps:
- Decode the call field into the actual call that we want to dispatch.
- Check with the SafeCallFilter on whether the execution of this call is allowed.
- Use the OriginConverter to convert the
MultiLocation
origin into aRuntimeOrigin
. - Check whether the call weight does not exceed
require_weight_at_most
. - Dispatch the call with the converted origin and set the
transact_status
register to be the result of the dispatch. - Calculate the weight that was actually used during the dispatch.
Example 1
For the full example, check the repo.
In this example, the relay chain executes the set_balance
function of pallet_balances
on
Parachain(1)
. This function requires the origin to be root. We enable the root origin for the
relay chain by setting ParentAsSuperuser
for the OriginConverter
config type.
let call = parachain::RuntimeCall::Balances(
pallet_balances::Call::<parachain::Runtime>::set_balance {
who: ALICE,
new_free: 5 * AMOUNT,
new_reserved: 0,
},
);
let message = Xcm(vec![
WithdrawAsset((Here, AMOUNT).into()),
BuyExecution { fees: (Here, AMOUNT).into(), weight_limit: WeightLimit::Unlimited },
Transact {
origin_kind: OriginKind::Superuser,
require_weight_at_most: Weight::from_parts(INITIAL_BALANCE as u64, 1024 * 1024),
call: call.encode().into(),
},
]);
Example 2
For the full example, check the repo.
In this example, as Parachain(1), we create an NFT collection on the relay chain and we then mint an NFT with ID 1. The admin for the nft collection is parachain(1). The call looks as follows:
let create_collection = relay_chain::RuntimeCall::Uniques(
pallet_uniques::Call::<relay_chain::Runtime>::create {
collection: 1u32,
admin: parachain_sovereign_account_id(1),
}
);
The owner of the NFT is Alice. The nft mint call looks as follows:
let mint = relay_chain::RuntimeCall::Uniques(
pallet_uniques::Call::<relay_chain::Runtime>::mint {
collection: 1u32,
item: 1u32,
owner: ALICE,
}
);
The xcm message contains the following instructions:
- Withdraw native assets from the
Parachain(1)
's sovereign account. - Buy weight with these assets.
- Create a collection with as admin and owner the sovereign account of
Parachain(1)
. - Mints an NFT in the collection with item ID 1 and as owner Alice.
let message = Xcm(vec![
WithdrawAsset((Here, AMOUNT).into()),
BuyExecution { fees: (Here, AMOUNT).into(), weight_limit: WeightLimit::Unlimited },
Transact {
origin_kind: OriginKind::SovereignAccount,
require_weight_at_most: Weight::from_parts(INITIAL_BALANCE as u64, 1024 * 1024),
call: create_collection.encode().into(),
},
Transact {
origin_kind: OriginKind::SovereignAccount,
require_weight_at_most: Weight::from_parts(INITIAL_BALANCE as u64, 1024 * 1024),
call: mint.encode().into(),
},
]);
Next:
Check out the following instructions that interact with the Transact Status Register: