Teleport Token across zkVerify Parachains
This guide provides step-by-step instructions to teleport tokens across zkVerify Parachains. Before you begin, please ensure you have a wallet setup correctly and ready to interact with Polkadot-JS.
XCM Teleport
A teleport is a powerful Cross-Consensus Message (XCM) instruction that facilitates the movement of assets between zkVerify Relay Chain and its System Parachains.
A teleport of an asset (VFY token) is an operation performed in two stages by an XCM executor: one part is executed on the Relay chain (burns the asset) and the other part on the Parachain (minting of the asset).

In this guide, we will be using teleport to move VFY tokens from zkVerify (Substrate-based) to VFlow (EVM-compatbile) and vice versa.
You can find more information on XCM here.
From zkVerify to VFlow via PolkadotJS-UI
From PolkadotJS navigate to Developer-> Extrinsics and select the xcmPallet pallet and the teleportAssets extrinsic:

You can see that we need to supply 4 parameters: dest, beneficiary, assets and feeAssetItem.
You can either use the Quick Teleport Guide or follow starting from the Destination section.
Quick Teleport Guide
Go to Developer -> Extrinsics -> Decode and copy/paste the following hex:
0x8c0105000100040500010300000000000000000000000000000000000000000005040000000000000000
Then from Decode move to Submission
You need to change the following parameters:
beneficiary -> V5 -> X1 -> AccountKey20 -> key: [u8, 20]: with the EVM address of the receiver on VFlowassets -> V5 -> 0 -> id -> fun -> Fungible: Compact<u128>: with the amount of VFY tokens you want to teleport to VFlow (specified with 18 digits)
Click on Submit Transaction and then Sign and Submit on the new window that will appear to conclude the teleport.
Destination

This parameter specifies the target chain where the assets are being teleported. Let's select:
- For
dest: XcmVersionedLocationselectV5from the drop-down menu. New fields will pop-up. Let's set:- For
parentsinput0 - For
interiorselectX1from the drop-down menu. Then new fields will appear. Let's set:- For
0selectParachainfrom the drop-down menu - We need finally to insert the ID of the VFlow
Parachain, which is1.
- For
- For
Beneficiary

This specifies the account that will receive the assets on the destination chain. Let's set:
- For
beneficiary: XcmVersionedLocationselectV5from the drop-down menu. New fields will pop-up. Let's set:- For
parentsinput0 - For
interiorinputX1from the drop-down menu. Then new fields will appear. Let's set:- For
0selectAccountKey20from the drop-down menu - For
keyinput the address of the Ethereum account you want to receive the assets on VFlow.
- For
- For
Assets

This defines the actual asset(s) and the amount(s) you are sending. Let's set:
- For
assets: XcmVersionedAssetsselectV5from the drop-down menu. Click on theAdd Itembutton. New fields will pop-up. Let's set:- For
parentsinput0 - For
interiorselectHerefrom the drop-down menu. Then new fields will appear. Let's set:- For
funselectFungiblefrom the drop-down menu - For
Fungibleinput the amount of assets you want to send. - Note: on-chain, amounts use 18 decimals. So if you want 1 VFY, you need to input 1 * 10^18 =
1000000000000000000.- Example:
- 1 VFY →
1000000000000000000 - 0.5 VFY →
500000000000000000
- 1 VFY →
- Example:
- For
- For
Fee Asset Item
This is simply the index of the asset in the assets array that will be used to pay for the XCM teleport fee. We only have a single asset (VFY) and we are using that one, so we set it to 0.
Submitting the Extrinsic
Click on Submit Transaction and then Sign and Submit on the new window that will appear to conclude the teleport.
From VFlow to zkVerify via PolkadotJS-UI
Note: if you want to bridge tokens back that you previously sent to an address living in Metamask, you must first export your private key from Metamask. You will then need to import this key into a wallet with full EVM and Substrate support, such as SubWallet (or Talisman).
To setup a SubWallet or Talisman wallet please refer to the documentation to setup a wallet ready to interact with Polkadot-JS.
The process here is exactly a mirror of what we did on zkVerify side.
The first thing you must do, is to connect to the VFlow parachain. Supposed you're already connected to zkVerify, click on the zkVerify logo top left of the screen, scroll down and find VFlow, then click on it, scroll up again and click Switch to connect.
In the case multiple EVM accounts are detected, you can select the beneficiary by clicking on the current selected account 0x... (under free balance), and choose from the drop-down menu.

Now that you're connected, from PolkadotJS navigate to Developer-> Extrinsics and select the zkvXcm pallet and the teleportAssets extrinsic.

Like before we need to supply 4 parameters: dest, beneficiary, assets and feeAssetItem.
You can either use the Quick Teleport Guide or follow starting from the Destination section.
Quick Teleport Guide
Go to Developer -> Extrinsics -> Decode and copy/paste the following hex:
0x1f010501000500010100486b90dbf0cb9bfe92b6ba7d4942019a17ada772ab5fa9258ac3df821daca54d050401000013000064a7b3b6e00d00000000
You need to change the following parameters:
beneficiary -> V5 -> X1 -> AccountId32 -> id: [u8, 32]: with the address of the receiver on zkVerifyassets -> V5 -> 0 -> id -> fun -> Fungible: Compact<u128>: with the amount of VFY tokens you want to teleport to zkVerify (specified with 18 digits)
Click on Submit Transaction and then Sign and Submit on the new window that will appear to conclude the teleport.
Destination
Let's start with the dest parameter:

- For
dest: XcmVersionedLocationselectV5from the drop-down menu. New fields will pop-up. Let's set:- For
parentsinput1 - For
interiorselectHerefrom the drop-down menu. Then new fields will appear. Let's set:
- For
Beneficiary
Let's set the beneficiary parameter as follows:

- For
beneficiary: XcmVersionedLocationselectV5from the drop-down menu. New fields will pop-up. Let's set:- For
parentsinput0 - For
interiorselectX1from the drop-down menu. Then new fields will appear. Let's set:- For
0selectAccountId32from the drop-down menu - For
idinput the address of the zkVerify account you want to receive the assets on zkVerify.
- For
- For
Assets
Let's set the assets parameter as follows:

- For
assets: XcmVersionedAssetsselectV5from the drop-down menu. Click on theAdd Itembutton. New fields will pop-up. Let's set:- For
parentsinput1 - For
interiorselectHerefrom the drop-down menu. Then new fields will appear. Let's set:- For
funselectFungiblefrom the drop-down menu - For
funinput the amount of assets you want to send. e.g. to send 1 VFY , remembering that the token has 18 decimals, it's:1000000000000000000.
- For
- For
Fee Asset Item
This is simply the index of the asset in the assets array that will be used to pay for the XCM teleport fee. We only have a single asset (VFY) and we are using that one, so we set it to 0.
Submitting the extrinsic
Click on Submit Transaction and then Sign and Submit on the new window that will appear to conclude our teleport.
From VFlow to zkVerify via EVM Tooling
We've included a precompile contract in VFlow that allows you to teleport VFY tokens to zkVerify directly from your standard EVM tools (like Metamask).
Contract is deployed at address 2060. Here is an example script leveraging web3 library:
const { Web3 } = require('web3');
// Configuration
const RPC_URL = 'wss://vflow-volta-rpc.zkverify.io'; // VFlow RPC endpoint
const PRIVATE_KEY = ''; // Your Ethereum account private key
const PRECOMPILE_ADDRESS = '0x000000000000000000000000000000000000080C'; // XCM Teleport precompile address
// XCM Teleport precompile ABI
const teleportABI = [
{
name: 'teleportToRelayChain',
type: 'function',
inputs: [
{ name: 'destinationAccount', type: 'bytes32' },
{ name: 'amount', type: 'uint256' },
],
outputs: [],
stateMutability: 'nonpayable',
},
];
async function testTeleport() {
// Initialize Web3
const web3 = new Web3(RPC_URL);
// Add your account to the wallet
const account = web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY);
web3.eth.accounts.wallet.add(account);
console.log(`Using account: ${account.address}`);
// Check balance
const balance = await web3.eth.getBalance(account.address);
console.log(`Account balance: ${web3.utils.fromWei(balance, 'ether')} tVFY`);
// Set up the contract
const contract = new web3.eth.Contract(teleportABI, PRECOMPILE_ADDRESS);
// Test parameters
const destinationAccount = ''; // 32-byte relay chain account
const amount = web3.utils.toWei('1', 'ether'); // 1 VFY
console.log(`Teleporting ${web3.utils.fromWei(amount, 'ether')} VFY`);
console.log(`From: ${account.address} (parachain)`);
console.log(`To: ${destinationAccount} (relay chain)`);
// Estimate gas
const gasEstimate = await contract.methods
.teleportToRelayChain(destinationAccount, amount)
.estimateGas({ from: account.address });
console.log(`Estimated gas: ${gasEstimate}`);
// Send transaction
console.log('Sending transaction...');
const result = await contract.methods
.teleportToRelayChain(destinationAccount, amount)
.send({
from: account.address,
gas: gasEstimate,
});
console.log('✅ Transaction successful!');
console.log(`Transaction hash: ${result.transactionHash}`);
console.log(`Block number: ${result.blockNumber}`);
console.log(`Gas used: ${result.gasUsed}`);
}
testTeleport();
A couple of important notes:
- In this case, the
amountto be sent, doesn't require to specify 18 decimals. - The
destinationAccountis an hex public key of zkVerify. While from PolkadotJS-UI you can use the AccountID and PolkadotJS automatically performs the conversion to the correct format, in this case you need to do it manually. From PolkadotJS navigate toDeveloper-> Utilitiesand select theConvert Addresstab:

Just copy paste the account id in the address to convert field to automatically get the public key to use as destinationAccount.
Teleport via zkv-xcm-library
We've developed a Typescript library, called zkv-xcm-library, in order to simplify the creation of such XCM teleport extrinsics and for ease of integration with your app/frontend. Check the readme for installation and usage instructions.
A Note on XCM Teleport Fees
Since an XCM message is executed both on the Relay Chain and Parachain side, both sender and receiver need to pay for execution fees. However:
- The fees charged to the sender are deducted directly from its main balance. This happens immediately when the transaction is included in a block, before the teleport's burn logic is even executed. So, if after fee deduction your remaining balance is not enough to cover the burn logic, the transaction will fail.
- The fees charged to the receiver are deducted from the amount being teleported. This happens when the XCM message is executed on the Parachain side.
Deep-Dive: XCM Parameters Explanation
Let's provide a brief explanation here of the complex XCM message construction and parameters we've seen before:
destprovides details about the destination of the message (we need to select the XCM version currently being used. V5 is the most-up-to-date at the time of writing).-
parentstells the XCM executor that the followinginterioris relative to the chain itself. -
interiordescribes the rest of the path, the specific, step-by-step directions to the final destination. It defines a sequence of "hops" or Junctions. Each junction is a specific location or entity within a consensus system.- The interior is structured as a container, usually X1, X2, X3, etc. where X stands for the number of Junctions (hops) in the path.
- Here: Equivalent to X0, no junctions.
- X1: A path with one junction.
- X2: A path with two junctions.
- ...and so on, up to X8.
- The interior is structured as a container, usually X1, X2, X3, etc. where X stands for the number of Junctions (hops) in the path.
-
You could think of it like a File System where:
- The root directory is the Relay Chain
- There are as many sub-directories as Parachains.
- In each directory there is a list of consensus entities (accounts, pallets, etc.) living in that Relay Chain/Parachain.

So setting parents = 0 means it's the current location (in bash it would be .) while parents = 1 means it's the parent location (in bash it would be ..).
Setting interior = X1 { Parachain: 3 } means essentially specifying ./parachain_3;
Instead setting interior = X2 { Parachain: 3, AccountKey20: 0x111 } means essentially specifying ./parachain_3/account_111
Putting it all together, in this case, we are initiating the teleport from the Relay Chain (so parents is 0, since we are already on the Relay Chain) to the Parachain with id 1, which is one hop away from the parent (so interior is X1 with Parachain set to 1).