XRPL Interchain Token Service

The XRPL Blockchain is unique in comparison to other blockchains integrated with Axelar. Unlike other blockchains, XRPL does not support smart contracts. As such there is no Interchain Token Service contract deployed on the chain the way there is with other EVM and non-EVM blockchain integrations; the same XRPL Multisig Gateway account that handles GMP also handles ITS transfers, and the deployment configs refer to that single address as both AxelarGateway and InterchainTokenService.

The integration leverages the XRPL Multisig Signing account type to facilitate token transfers. The gateway is controlled by the Axelar Verifier set.

The testnet address for the XRPL Gateway is: rNrjh1KGZk2jBR3wPfAQnoidtFFYQKbQn2.

The mainnet address for the XRPL Gateway is: rfmS3zqrQrka8wVyhXifEeyTwe8AMz2Yhw.

Axelar’s integration to XRPL allows for the cross-chain transfer of both the XRPL native token (XRP itself) as well as custom tokens (IOU).

The interchain transfer flow is similar to that of the regular GMP flow. As with the regular GMP flow, you send a payment transaction to the Gateway account, but with interchain_transfer as the type memo rather than call_contract.

The Gateway becomes the custodian of your token on XRPL while the transfer is routed via the ITS Hub to the destination chain (and back, when bridged back).

You attach the following memos to the payment. Each field name goes into MemoType and the value into MemoData, both hex-encoded ASCII.

  1. type: always interchain_transfer for ITS transfers.
  2. destination_address: the recipient address on the destination chain, hex-encoded without any 0x prefix.
  3. destination_chain: the Axelar chain name of the destination chain (for example xrpl-evm, ethereum, hedera).
  4. gas_fee_amount: the portion of Amount set aside to pay cross-chain transaction fees. See Encoding gas_fee_amount for the required format.
  5. payload: ABI-encoded calldata for the destination Executable. Include this memo only when the destination address is a smart contract that you want to invoke. When the destination is an externally-owned account (EOA) or any address that is not an executing contract, omit the payload memo entirely; an empty or zero-length payload is still treated as a GMP call and will fail.

A single XRPL Payment carries one Amount. The validators interpret that Amount as both the amount to bridge and the source of gas:

transfer_amount = Amount - gas_fee_amount

The transfer_amount (in the same currency as Amount) is what the ITS Hub forwards to the destination chain. The gas_fee_amount portion is credited to the Axelar gas service to pay relayers along the cross-chain route.

Two consequences worth keeping in mind:

  • The initial gas allocation is paid in the same asset that is being sent. A single XRPL Payment cannot carry an Amount and a gas_fee_amount in different currencies. If you later need to top up gas (for example because the initial allocation was too low, or because you want to add XRP gas to an IOU transfer), use Add Gas, which is a separate Payment and accepts any supported token.
  • Amount must include the gas. If you want the recipient on the destination chain to receive X tokens, then Amount = X + gas_fee_amount on the source side.

💡

gas_fee_amount must be strictly less than Amount. Otherwise Axelar verifiers reject the message, the payment is never indexed on Axelarscan, and the funds remain at the Gateway multisig without an automated refund path.

The format of the gas_fee_amount memo depends on the type of Amount in the Payment. There is no per-memo currency or issuer field; those are inherited from Amount.

Amount typeSource tokengas_fee_amount memo format
Drops string, e.g. "1000000"XRPAn integer drops string (parsed as a u64), e.g. "100000" for 0.1 XRP gas.
Issued objectIOUA decimal string (parsed as an XRPLTokenAmount) in the same currency and issuer as Amount.

💡

A memo that does not match the format expected for the Amount type (for example a decimal like "0.18" in an XRP/drops payment) cannot be verified by Axelar, so the message is never indexed on Axelarscan and Add Gas cannot recover it.

This example sends 1 XRP, allocating 0.1 XRP to gas. The destination chain receives the equivalent of 0.9 XRP.

{
TransactionType: "Payment",
Account: "<user XRPL address>",
Destination: "<gateway XRPL address>",
Amount: "1000000", // 1 XRP, in drops
Memos: [
{
Memo: {
MemoType: "74797065", // hex("type")
MemoData: "696E746572636861696E5F7472616E73666572", // hex("interchain_transfer")
},
},
{
Memo: {
MemoType: "64657374696E6174696F6E5F61646472657373", // hex("destination_address")
MemoData: "30413930633041663142303766364143333466333532303334384462666165373342446133353845", // hex("0A90c0Af1B07f6AC34f3520348Dbfae73BDa358E"), an EVM address with no 0x prefix
},
},
{
Memo: {
MemoType: "64657374696E6174696F6E5F636861696E", // hex("destination_chain")
MemoData: "7872706C2D65766D", // hex("xrpl-evm")
},
},
{
Memo: {
MemoType: "6761735F6665655F616D6F756E74", // hex("gas_fee_amount")
MemoData: "313030303030", // hex("100000"), i.e. 100000 drops = 0.1 XRP
},
},
],
}

Axelar parses the gas_fee_amount memo as a u64 drops value (0.1 XRP), computes transfer_amount = 1 XRP - 0.1 XRP = 0.9 XRP, and the ITS Hub forwards the equivalent of 0.9 XRP to the destination chain.

The hex casing of MemoType and MemoData does not matter on submission; the XRPL canonical wire format is uppercase, which is what XRPL nodes and explorers will return.

This example sends 1 of an IOU (currency ABC, issuer r4DVHyEisbgQRAXCiMtP2xuz5h3dDkwqf1), allocating 0.05 to gas. The destination chain receives the equivalent of 0.95 ABC.

{
TransactionType: "Payment",
Account: "<user XRPL address>",
Destination: "<gateway XRPL address>",
Amount: {
currency: "ABC", // IOU currency code
issuer: "r4DVHyEisbgQRAXCiMtP2xuz5h3dDkwqf1", // IOU issuer
value: "1", // 1 ABC, including the gas portion
},
Memos: [
{
Memo: {
MemoType: "74797065", // hex("type")
MemoData: "696E746572636861696E5F7472616E73666572", // hex("interchain_transfer")
},
},
{
Memo: {
MemoType: "64657374696E6174696F6E5F61646472657373", // hex("destination_address")
MemoData: "30413930633041663142303766364143333466333532303334384462666165373342446133353845", // hex("0A90c0Af1B07f6AC34f3520348Dbfae73BDa358E"), an EVM address with no 0x prefix
},
},
{
Memo: {
MemoType: "64657374696E6174696F6E5F636861696E", // hex("destination_chain")
MemoData: "7872706C2D65766D", // hex("xrpl-evm")
},
},
{
Memo: {
MemoType: "6761735F6665655F616D6F756E74", // hex("gas_fee_amount")
MemoData: "302E3035", // hex("0.05"), i.e. 0.05 ABC
},
},
],
}

Axelar parses the gas_fee_amount memo as an XRPLTokenAmount in the same (currency, issuer) as Amount (here 0.05 ABC), computes transfer_amount = 1 - 0.05 = 0.95 ABC, and the ITS Hub forwards 0.95 ABC to the destination chain.

To trigger an Executable on the destination chain, include a fifth memo of type payload with the ABI-encoded calldata:

{
Memo: {
MemoType: "7061796C6F6164", // hex("payload")
// ABI-encoded calldata to invoke the destination Executable; example payload below encodes the string "GMP works too?"
MemoData: "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000E474D5020776F726B7320746F6F3F000000000000000000000000000000000000",
},
}

The destination_address for this case is the contract address of the Executable on the destination chain. Omit the payload memo for plain token transfers.

A full example can be found here.

The example makes use of the XRPL Client library, which simplifies connecting to the XRPL network, building transactions, signing them, and submitting them.

The key function in this example is interchainTransfer():

async function interchainTransfer(_config, wallet, client, chain, options, args) {
await client.sendPayment(
wallet,
{
destination: chain.contracts.InterchainTokenService.address,
amount: parseTokenAmount(args.token, args.amount), // token is either "XRP" or "<currency>.<issuer-address>"
memos: [
{ memoType: hex('type'), memoData: hex('interchain_transfer') },
{ memoType: hex('destination_address'), memoData: hex(args.destinationAddress.replace('0x', '')) },
{ memoType: hex('destination_chain'), memoData: hex(args.destinationChain) },
{ memoType: hex('gas_fee_amount'), memoData: hex(options.gasFeeAmount) },
...(options.payload ? [{ memoType: hex('payload'), memoData: options.payload }] : []),
],
},
options,
);
}

Note that hex(options.gasFeeAmount) performs no format validation. Make sure the string you pass matches the format required by the Amount type as set out in Encoding gas_fee_amount.

To send 1 XRP with 0.1 XRP allocated to gas:

Terminal window
node xrpl/interchain-transfer.js -e testnet -n xrpl XRP 1 xrpl-evm 0x312dba807EAE77f01EF3dd21E885052f8F617c5B --gasFeeAmount 100000

--gasFeeAmount is an integer drops string (100000 drops = 0.1 XRP).

To send 1 ABC.r4DVHyEisbgQRAXCiMtP2xuz5h3dDkwqf1 with 0.05 ABC allocated to gas:

Terminal window
node xrpl/interchain-transfer.js -e testnet -n xrpl ABC.r4DVHyEisbgQRAXCiMtP2xuz5h3dDkwqf1 1 xrpl-evm 0x312dba807EAE77f01EF3dd21E885052f8F617c5B --gasFeeAmount 0.05

--gasFeeAmount is a decimal string in the same currency as the token argument.

Once the command is run, you can track the cross-chain transaction on the Axelarscan explorer.

If gas_fee_amount is well-formed but not enough to cover the actual Axelar-side relaying costs, the message is indexed on the GMP API as INSUFFICIENT_GAS and you can unblock it by topping up via Add Gas.

This recovery path only works for messages that Axelar verifiers were able to verify in the first place. Payments whose gas_fee_amount is malformed or not strictly less than Amount are never indexed on Axelarscan and cannot be recovered through Add Gas; validate the memo format on the source side to avoid that case.

Edit on GitHub