This article will explain how to transfer tokens across chains. It assumes a side chain is already deployed and been indexed by the main-chain.

The transfer will always use the same contract methods and the following two steps:

  • initiate the transfer

  • receive the tokens

Initiate the transfer

On the token contract, it's the CrossChainTransfer method that is used to trigger the transfer:

rpc CrossChainTransfer (CrossChainTransferInput) returns (google.protobuf.Empty) { }
message CrossChainTransferInput {
aelf.Address to = 1;
string symbol = 2;
sint64 amount = 3;
string memo = 4;
int32 to_chain_id = 5;
int32 issue_chain_id = 6;

Let's review the fields of the input:

  • to: this is the address on the destination chain that will receive the tokens.

  • symbol and amount: the token and amount to be transferred.

  • issue_chain_id and to_chain_id: respectively the source (the chain on which the token was issued) and destination chain id (destination is the chain on which the tokens will be received).

Receive on the destination chain

On the destination chain tokens need to be received, it's the CrossChainReceiveToken method that is used to trigger the reception:

rpc CrossChainReceiveToken (CrossChainReceiveTokenInput) returns (google.protobuf.Empty) { }
message CrossChainReceiveTokenInput {
int32 from_chain_id = 1;
int64 parent_chain_height = 2;
bytes transfer_transaction_bytes = 3;
aelf.MerklePath merkle_path = 4;
rpc GetBoundParentChainHeightAndMerklePathByHeight (aelf.SInt64Value) returns (CrossChainMerkleProofContext) {
option (aelf.is_view) = true;
message CrossChainMerkleProofContext {
int64 bound_parent_chain_height = 1;
aelf.MerklePath merkle_path_from_parent_chain = 2;

Let's review the fields of the input:

  • from_chain_id: the source chain id (the chain that issued the tokens).

  • parent_chain_height:

    • main-chain to side-chain: the height of the block on the source chain that includes the CrossChainTransfer transaction (or more precisely, the block that indexed the transaction).

    • side-chain to side-chain or side-chain to main-chain: this height is the result of GetBoundParentChainHeightAndMerklePathByHeight (input is the height of the CrossChainTransfer) - accessible in the bound_parent_chain_height field.

  • transfer_transaction_bytes: the serialized form of the CrossChainTransfer transaction.

  • merkle_path: the cross-chain merkle path. For this, two cases to consider:

    • main-chain to side-chain transfer: for this you just need the merkle path from the main-chain's web api with the GetMerklePathByTransactionIdAsync method (CrossChainTransfer transaction ID as input).

    • side-chain to side-chain or side-chain to main-chain: for this you also need to get the merkle path from the source node (side-chain here). But you also have to complete this merkle path with GetBoundParentChainHeightAndMerklePathByHeight with the cross-chain CrossChainTransfer transaction's block height (concat the merkle path nodes). The nodes are in the merkle_path_from_parent_chain field of the CrossChainMerkleProofContext object.