Axelar<>XRPL Integration Overview
The Axelar<>XRPL integration enables message passing between XRPL and other Axelar-supported blockchains via the Axelar Amplifier network. Amplifier is a set of CosmWasm contracts on the Axelar chain that lets any external chain connect to the Axelar interchain network without changes to Axelar's core. Each connected chain plugs in through a small group of per-chain contracts (Gateway, Voting Verifier, and Multisig Prover) used to verify and route messages to a destination chain. The integration allows transferring XRP and tokens issued on XRPL to other supported chains and back. It also supports bridging non-XRPL tokens between their native blockchain and XRPL, as well as sending messages to other chains (General Message Passing).
XRPL is different from other chain integrations because it has no smart contract layer. Instead, the XRPL gateway is implemented as a multisig account whose signers are the active Axelar verifier set, and that signer set is kept in sync by SignerListSet transactions on XRPL. Users initiate cross-chain actions by sending an XRPL Payment to that account with structured Memos; the Amplifier side then processes the transaction through the three XRPL-specific contracts (xrpl-gateway, xrpl-voting-verifier, xrpl-multisig-prover), along with the generic Amplifier contracts and the ITS Hub.
High Level Architecture
Incoming Flow
flowchart TD
subgraph Axelar
G1{"xrpl-gateway"}
G2{"Gateway"}
Vo{"xrpl-voting-verifier"}
R{"Router"}
S{"Service Registry"}
end
Relayer --"VerifyMessages([M1,M2])"-->G1
G1 --"VerifyMessages([M1,M2])"--> Vo
Vo --"ActiveVerifiers"--> S
Verifiers --"Vote(poll_id, votes)"--> Vo
Relayer --"RouteIncomingMessages([M1,M2])"-->G1
G1 --"RouteMessages([M1,M2])"-->R
R --"RouteMessages([M1,M2])"-->G2
Outgoing Flow
flowchart TD
subgraph Axelar
G2{"xrpl-gateway"}
P{"xrpl-multisig-prover"}
M{"Multisig"}
S{"Service Registry"}
end
Relayer --"ConstructProof(cc_id, payload)"-->P
P --"OutgoingMessages([M1.id,M2.id])"-->G2
P --"ActiveVerifiers"-->S
P --"StartSigningSession(verifier_set_id, payload_hash)"-->M
Verifiers --"SubmitSignature(session_id, signature)"-->M
Relayer --"Proof(multisig_session_id)" --> P
P --"Multisig(session_id)"-->M
For the message-level walkthroughs of each direction, see Message Flows to and from XRPL.
Contract Overview
Gateway
xrpl-gateway is the entry and exit point on Axelar for XRPL traffic. It accepts inbound XRPL messages from the relayer for verification, routes verified user-payment variants through the router (and through the ITS Hub for token transfers), confirms gas top-ups, and stores outbound messages from the router for the multisig prover to pick up. It also acts as the ITS edge for XRPL: it holds the token-id registry, queries the ITS Hub for destination decimals, scales amounts between chains, and tracks accrued gas per token id.
Voting Verifier
xrpl-voting-verifier runs stake-weighted polls over XRPL transactions for all five XRPLMessage variants. When the xrpl-gateway calls VerifyMessages, the voting verifier opens a poll, takes a weighted snapshot of the active verifier set from the service registry, and emits a messages_poll_started event. Off-chain verifiers cast their votes; once quorum is reached, the contract emits wasm-quorum_reached, which is what triggers the relayer's next action (routing or confirmation, depending on the message variant).
Multisig Prover
xrpl-multisig-prover builds the XRPL transactions that the multisig account needs to send: Payment for token deliveries, SignerListSet for verifier set rotation, TicketCreate to refill the ticket pool, and TrustSet to open trust lines for XRPL-native IOUs. It opens signing sessions on the generic multisig contract, manages the available-ticket pool and sequence numbers, tracks the XRP fee reserve that funds outbound transaction fees, and holds the current/next verifier set.
Router
router maintains the registry of connected chains and their gateways and routes verified messages between them. The xrpl-gateway is a registered chain gateway and uses it both inbound (delivering verified messages on toward the ITS Hub or directly to a destination chain) and outbound (receiving messages whose destination is XRPL from other source chains).
Multisig
multisig is the generic CosmWasm contract on Axelar that coordinates signing sessions. The xrpl-multisig-prover creates a session whenever it builds a new XRPL transaction; verifiers submit per-signer signature shares; once quorum is met the contract emits signing_completed and publishes the assembled signatures.
Service Registry
service-registry tracks verifier bonding, authorization, and per-chain support. The xrpl-voting-verifier and xrpl-multisig-prover both query it for the active verifier set when opening polls and signing sessions.
Coordinator
coordinator tracks per-chain prover registrations and the verifier sets active on each chain. It serves as the unbonding gate for the service registry, so verifiers cannot withdraw stake while still active on any chain.
Axelarnet Gateway
The Axelarnet Gateway (in the upstream repo) is the contract through which Axelar-resident contracts such as the ITS Hub send and receive cross-chain messages via the generic router. Every ITS message in either direction between XRPL and another chain passes through it.
ITS Hub
The ITS Hub (in the upstream repo) is the chain-agnostic router for Interchain Token Service messages. It validates the source ITS edge, tracks per-chain token supply, and rescales amounts between chains with different decimal precisions. The xrpl-gateway sends SEND_TO_HUB envelopes to this contract when translating an XRPL inbound interchain transfer; the Hub re-wraps them as RECEIVE_FROM_HUB for delivery to the destination chain. The same machinery runs in reverse when a source chain sends an ITS transfer destined for XRPL.
Message Semantics
The xrpl-gateway accepts an XRPL-native union type, XRPLMessage, whose variants capture the five categories of XRPL transactions the bridge cares about. User-facing variants like InterchainTransferMessage and CallContractMessage are translated into generic router_api::Message objects and forwarded through the router. AddGasMessage is also user-initiated: it tops up gas for an existing cross-chain message and is confirmed on the gateway, but it does not become a router message. Operational/control variants such as AddReservesMessage and ProverMessage are handled directly by the XRPL-specific contracts and also never become router messages.
The full definition lives in upstream packages/xrpl-types/src/msg.rs.
#![allow(unused)] fn main() { pub enum XRPLMessage { InterchainTransferMessage(XRPLInterchainTransferMessage), CallContractMessage(XRPLCallContractMessage), AddGasMessage(XRPLAddGasMessage), AddReservesMessage(XRPLAddReservesMessage), ProverMessage(XRPLProverMessage), } }
CrossChainId construction on XRPL
The CrossChainId struct itself (defined in upstream packages/router-api/src/primitives.rs) is the generic Amplifier identifier shared across all chains. What differs is how its message_id field is populated for XRPL traffic: for InterchainTransferMessage and CallContractMessage, the xrpl-gateway constructs the cc_id as:
#![allow(unused)] fn main() { CrossChainId { source_chain: "xrpl", message_id: "0x<hex of the 32-byte XRPL tx hash>", } }
The transaction hash is XRPL's SHA-512-half of the signed transaction (32 bytes), rendered as a 0x-prefixed hex string. The other three variants do not become generic router messages and therefore have no associated CrossChainId.
Inbound user payments
InterchainTransferMessage and CallContractMessage are the two variants reported by the relayer when a user sends an XRPL Payment to the multisig gateway account. They are verified by the xrpl-voting-verifier and routed by the xrpl-gateway.
#![allow(unused)] fn main() { pub struct XRPLInterchainTransferMessage { pub tx_id: HexTxHash, // the user's XRPL Payment tx hash pub source_address: XRPLAccountId, // the XRPL user (20-byte account id) pub destination_chain: ChainNameRaw, pub destination_address: nonempty::String, // raw bytes of destination addr, hex pub payload_hash: Option<[u8; 32]>, // optional, only set if a payload memo is present pub transfer_amount: XRPLPaymentAmount, // tokens to deliver (excluding gas) pub gas_fee_amount: XRPLPaymentAmount, // gas, in the same denomination } pub struct XRPLCallContractMessage { pub tx_id: HexTxHash, pub source_address: XRPLAccountId, pub destination_chain: ChainNameRaw, pub destination_address: nonempty::String, pub payload_hash: [u8; 32], // required; pure GMP always has a payload pub gas_fee_amount: XRPLPaymentAmount, // equal to the Payment Amount (no transfer component) } }
Inbound user/operator top-ups
AddGasMessage lets a user top up the gas allocation of an existing cross-chain message they previously initiated. AddReservesMessage lets an operator (typically the relayer itself) top up the multisig account's XRP reserve. Both are verified by the xrpl-voting-verifier; AddGasMessage is confirmed on the xrpl-gateway, AddReservesMessage on the xrpl-multisig-prover. Neither produces a router_api::Message.
#![allow(unused)] fn main() { pub struct XRPLAddGasMessage { pub tx_id: HexTxHash, pub msg_id: HexTxHash, // tx_id of the original message being topped up pub amount: XRPLPaymentAmount, pub source_address: XRPLAccountId, } pub struct XRPLAddReservesMessage { pub tx_id: HexTxHash, pub amount: u64, // XRP-only, denominated in drops } }
Outbound prover confirmation
ProverMessage is the inverse direction: the multisig account has just submitted a prover-built XRPL transaction (a Payment, SignerListSet, TicketCreate, or TrustSet) and the relayer is reporting that inclusion back to Axelar so the xrpl-multisig-prover can release the ticket, clear the payload, and (for SignerListSet) promote the new verifier set.
#![allow(unused)] fn main() { pub struct XRPLProverMessage { pub tx_id: HexTxHash, // the on-ledger tx hash pub unsigned_tx_hash: HexTxHash, // the canonical-serialization hash; used to match the prover's stored payload } }
Supporting XRPL types
These types appear inside the message variants above and are also defined in packages/xrpl-types.
#![allow(unused)] fn main() { /// 20-byte XRPL account id. Display form is the base58 classic address (rXXX...). pub struct XRPLAccountId([u8; 20]); /// Either native XRP (drops) or an issued currency amount. /// See "Supported Values and Tokens" for the full conversion rules. pub enum XRPLPaymentAmount { Drops(u64), // native XRP Issued(XRPLToken, XRPLTokenAmount), // IOU } pub struct XRPLToken { pub issuer: XRPLAccountId, pub currency: XRPLCurrency, } /// 160-bit XRPL currency code (3-char ASCII or 40-char hex form). pub struct XRPLCurrency([u8; 20]); /// XRPL's mantissa+exponent format. See "Supported Values and Tokens" /// for the canonicalization rules. pub struct XRPLTokenAmount { mantissa: u64, exponent: i64, } }
For the full conversion rules between these XRPL amount types and Uint256 (the form the ITS Hub uses), and for the constraints on currency codes, addresses, and trust lines, see Supported Values and Tokens on XRPL.
Message Flows to and from XRPL
This document gives a high level walkthrough of what happens when a cross chain message moves from XRPL to another chain connected to Axelar, or from another chain to XRPL. Each on-Axelar contract mentioned below links inline to its per-contract page.
The integration uses the same Axelar Amplifier protocol that other chains plug into, but XRPL has a unique design because it does not have smart contracts. Everything that would normally be done by an on chain "gateway" contract on the external chain is done instead by a single XRPL multisig account, controlled by the active Axelar verifier set. So, unlike most other integrations where the chain-specific logic lives on the edge chain contracts, in this case the XRPL-specific logic lives in three CosmWasm contracts deployed on Axelar.
For unfamiliar XRPL or Axelar terminology, the glossary gives a short definition. Linked terms throughout the document point into that glossary.
Three flows are described below. Flow A covers a pure general message passing (GMP) call originating on XRPL with no token transfer attached. Flow B covers an Interchain Token Service (ITS) transfer from XRPL to a destination chain. Flow C covers an ITS transfer in the reverse direction: from a source chain to XRPL. There is no inbound pure GMP flow because XRPL has no executable destination; only ITS token deliveries land on XRPL.
Three Axelar resident contracts do the XRPL specific work
The three XRPL contracts absorb every responsibility that would otherwise live in chain side contracts on a smart contract chain:
xrpl-gatewayis the entry and exit point for XRPL messages on Axelar. It is also the ITS edge for XRPL (the role that on other chains is split out into a separate contract). It accepts inbound messages from a relayer, asks the voting verifier to confirm them, translates them into ITS Hub messages, and forwards them through the router. In the other direction it stores outbound messages received from the router for the prover to pick up.xrpl-voting-verifierruns polls over XRPL transactions. The off chain XRPL handler reads the ledger via JSON RPC and votes.xrpl-multisig-proverbuilds the XRPL transactions that the multisig account needs to send, opens signing sessions on the genericmultisigcontract, and tracks XRPL specific state (the ticket pool, the reserve budget, trust lines, and the current and next verifier set).
The rest of the protocol is unchanged from the upstream Amplifier. The generic router, multisig, service-registry, and coordinator contracts behave exactly as they do for any other chain. The Axelarnet Gateway and the ITS Hub live in the canonical axelar-amplifier repository and are shared infrastructure used by every chain.
Off chain, two distinct processes do the work that bridges the two ledgers:
- The verifiers running ampd: the same off chain daemon every Amplifier verifier already runs for every chain. For XRPL they additionally run an XRPL specific handler binary that watches the Axelar chain for poll events, looks up the cited XRPL transactions via JSON RPC, and votes. The same handler also signs outbound XRPL transactions when the multisig prover opens a signing session.
- The XRPL relayer: a set of Rust services that monitor the XRPL ledger for activity on the multisig account, push relevant transactions to the Axelar chain contracts, and submit prover produced XRPL transactions back to the ledger. Relaying is permissionless; anyone can run the same software.
How a user signals intent on XRPL
Because XRPL cannot host a callContract style entry point, the user expresses their intent through the only primitive XRPL gives them: an XRPL Payment transaction. The payment is addressed to the multisig account. Structured Memos on the payment tell the bridge what to do.
For an interchain token transfer the memo schema is:
| Memo key | Required | Meaning |
|---|---|---|
type | yes | The literal interchain_transfer (or call_contract for a pure GMP call) |
destination_chain | yes | The case sensitive chain name |
destination_address | yes | The recipient address on the destination chain, hex encoded, without any leading 0x |
gas_fee_amount | yes | How much of the payment is gas, in the same denomination as the Amount field |
payload | optional | An ABI encoded payload, only present for general message passing calls that carry data |
The full Amount field of the Payment is the sum of the transfer amount and the gas fee. The gas fee is later deducted on Axelar by the gateway when accounting accrued gas per token id.
Flow A: GMP from XRPL to a destination chain
A user on XRPL wants to make a pure general message passing call to a contract on another Axelar connected chain. No tokens are transferred; only an opaque payload is delivered to the destination contract. This is signalled with the call_contract memo type. The entire Amount of the XRPL Payment is treated as the gas fee for the cross chain call.
Sequence
sequenceDiagram
autonumber
actor User
participant XRPL as XRPL ledger
participant Relayer as XRPL relayer
box LightYellow Axelar
participant XGW as xrpl-gateway
participant XVV as xrpl-voting-verifier
participant Router
end
actor Verifiers
participant Dest as Destination chain side
User->>XRPL: Payment to multisig with Memos (type=call_contract, payload, ...)
Relayer->>XRPL: poll account_tx
XRPL-->>Relayer: new payment detected
Relayer->>XGW: VerifyMessages (CallContractMessage)
XGW->>XVV: VerifyMessages
XVV-->>Verifiers: emit messages_poll_started
Verifiers->>XRPL: tx RPC lookup
XRPL-->>Verifiers: validated tx data
Verifiers->>XVV: Vote
Note over XVV: quorum reached
XVV-->>Relayer: wasm-quorum_reached event (delivered by the Axelar GMP API)
Relayer->>XGW: RouteIncomingMessages
XGW->>XGW: verify payload hash, build plain Message
XGW->>Router: route message
Router->>Dest: deliver via destination gateway
Note over Dest: destination prover signs, relayer submits,<br/>destination contract executes the call
General steps
-
User Payment to the multisig account. The user signs and submits an XRPL
Paymentto the multisig account. Required memos:type=call_contract,destination_chain,destination_address(raw bytes of the destination contract, hex encoded),gas_fee_amount(equal toAmount, since the whole payment is gas), andpayload(ABI encoded calldata for the destination contract). The XRPL transaction hash becomes the cross chain id for the rest of the flow. -
Off chain detection. The XRPL relayer polls
account_txon the multisig account, sees the new payment, classifies it by memotypeas aCallContractMessage, and callsVerifyMessageson thexrpl-gateway. -
The voting verifier opens a poll. The gateway forwards the
CallContractMessageto thexrpl-voting-verifier, which takes a weighted snapshot of the active verifiers for chainxrpland emits amessages_poll_startedevent. -
Verifiers vote. Each verifier running
ampdand the XRPL handler fetches the cited XRPL transaction and checks: the transaction isvalidated, its destination is the multisig account,delivered_amountequalsAmount(defends againsttfPartialPayment), the memos parse correctly, thegas_fee_amountequalsAmount(no transfer component), andkeccak256(payload)matches the message's payload hash. The handler casts aVote. As soon as quorum is reached, the voting verifier emitswasm-quorum_reached. -
Routing to the destination chain. The Axelar GMP API pushes the
wasm-quorum_reachedtask to the relayer, which callsRouteIncomingMessageson thexrpl-gateway. For each verifiedCallContractMessagethe gateway builds a plainrouter_api::Messagecarrying the originalsource_address(the XRPL user), the user supplieddestination_chainanddestination_address, andpayload_hash = keccak256(payload). The message is handed straight to the genericrouter. -
Outbound GMP leg. From here the flow is identical to any other outbound Amplifier GMP. The router delivers to the destination chain's gateway slot; the destination chain's
multisig-proverconstructsexecute_datafor that chain's gateway; the genericmultisigcontract collects signatures; a relayer broadcasts theexecute_datato the destination's Axelar Gateway. The upstream Amplifier overview describes this leg. -
Delivery on the destination chain. The destination's Axelar Gateway approves the message; the executor invokes the application contract at
destination_addresswith thepayload.
About ITS
The remaining two flows use the Interchain Token Service (ITS), a chain agnostic protocol layered on top of Amplifier GMP that handles the deployment, linking, and transfer of tokens across chains. ITS adds two pieces to a generic GMP message: a per-token identifier shared across chains, and a central routing contract on Axelar (the ITS Hub) that validates, tracks per chain supply, and rescales amounts between chains with different decimal precisions. The protocol is described in the upstream ITS Hub repository.
A very common ITS interaction on XRPL is moving native XRP to a smart contract chain and back, using Wrapped XRP (wXRP) as the destination representation such as XRPL-EVM:
- From XRPL to XRPL-EVM: a user pays native XRP to the XRPL multisig account; the XRP is locked in the multisig (the multisig is the gateway, not the issuer); on the destination chain, the local ITS edge mints wXRP to the recipient.
- From XRPL-EVM back to XRPL: a user calls
interchainTransferon the XRPL-EVM ITS edge with their wXRP; the wXRP is burned locally; the XRPL multisig sends an equivalent amount of native XRP from its balance to the recipient on XRPL.
The same mechanism applies to other tokens: an XRPL-native IOU bridged out is locked on XRPL and minted on the destination, and the reverse direction is the symmetric burn-and-unlock. The two flows below describe each direction in detail.
Flow B: ITS transfer from XRPL to a destination chain
A user on XRPL wants to send value (native XRP or an IOU) to a recipient on another Axelar connected chain. In this flow XRPL is the source chain and the other chain is the destination. The route goes through the ITS Hub.
Sequence
sequenceDiagram
autonumber
actor User
participant XRPL as XRPL ledger
participant Relayer as XRPL relayer
box LightYellow Axelar
participant XGW as xrpl-gateway
participant XVV as xrpl-voting-verifier
participant Router
participant Hub as ITS Hub
end
actor Verifiers
participant Dest as Destination chain side
User->>XRPL: Payment to multisig with Memos (type=interchain_transfer, ...)
Relayer->>XRPL: poll account_tx
XRPL-->>Relayer: new payment detected
Relayer->>XGW: VerifyMessages (InterchainTransferMessage)
XGW->>XVV: VerifyMessages
XVV-->>Verifiers: emit messages_poll_started
Verifiers->>XRPL: tx RPC lookup
XRPL-->>Verifiers: validated tx data
Verifiers->>XVV: Vote
Note over XVV: quorum reached
XVV-->>Relayer: wasm-quorum_reached event (delivered by the Axelar GMP API)
Relayer->>XGW: RouteIncomingMessages
XGW->>Hub: query destination decimals
Hub-->>XGW: decimals
XGW->>XGW: scale amount, wrap as SEND_TO_HUB
XGW->>Router: route message
Router->>Hub: deliver via Axelarnet Gateway
Hub->>Hub: validate, decrement source supply,<br/>rescale, wrap as RECEIVE_FROM_HUB
Hub->>Router: route to destination chain
Router->>Dest: deliver via destination gateway
Note over Dest: destination prover signs,<br/>relayer submits, ITS edge mints or unlocks
Dest-->>User: tokens delivered
General steps
-
User Payment to the multisig account. The user signs and submits an ordinary XRPL
Paymenttransaction whose destination is the multisig account. The transaction's memos encode the destination chain, the destination address (hex encoded raw bytes, since address formats differ across chains), the amount to allocate as gas, and optionally a payload for a GMP call. The XRPL transaction hash becomes the cross chain id for the rest of the flow. The user's own XRPL key signs the transaction; the multisig account is the recipient, not the sender. -
Off chain detection. The XRPL relayer polls the multisig account's transaction history via XRPL's
account_txRPC. When it detects a new payment, it classifies it by memotypeand constructs anInterchainTransferMessage(orCallContractMessagefor a pure GMP payload). It then callsVerifyMessageson thexrpl-gateway. -
The voting verifier opens a poll. The gateway forwards the messages to the
xrpl-voting-verifier, which takes a snapshot of the active verifier set from the service registry for chainxrpland opens a poll. Amessages_poll_startedevent is emitted, carrying the candidate messages. -
Verifiers vote. Each verifier running
ampdand the XRPL handler binary sees the poll event, fetches the cited XRPL transaction over its own XRPL JSON RPC connection, and checks several invariants: the transaction isvalidated, its destination is the multisig account,delivered_amountequalsAmount(which guards against the XRPLtfPartialPaymentexploit), the memos parse correctly, and the resulting message matches the one under poll. The handler then casts aVote. The moment a vote pushes the weighted tally past the quorum threshold, the voting verifier emits awasm-quorum_reachedevent and the message becomes queryable asSucceededOnSourceChain(the formalEndPollcall happens later, on a separate path). -
Routing into the ITS Hub. Once the Axelar GMP API picks up the
wasm-quorum_reachedevent,RouteIncomingMessagesis called on thexrpl-gateway. For each verified message the gateway looks up the token id the message refers to, queries the ITS Hub for the destination chain's registered decimals so it can scale the amount correctly, and constructs aHubMessage::SendToHub(InterchainTransfer)payload. The wrapped message is handed to the genericrouter, which delivers it to the Axelarnet Gateway. The Axelarnet Gateway hands it to the ITS Hub for processing. -
ITS Hub processing. The Hub validates that the source address is the registered XRPL ITS edge, decrements the per chain supply tally for the token, applies any further decimal scaling required by the destination chain, checks that neither side is frozen, and re wraps the inner message as
HubMessage::ReceiveFromHub(InterchainTransfer). It then callsCallContracton the Axelarnet Gateway again, addressed to the ITS edge on the destination chain. -
Second GMP leg leaves Axelar. From this point the message is an ordinary outbound Amplifier GMP toward the destination chain. The destination chain's
multisig-proverconstructs anexecute_datablob, the genericmultisigcontract collects signatures from the active verifier set, and a relayer broadcasts the resulting calldata to the destination chain's gateway. This is the same machinery used by every other Amplifier chain. The upstream Amplifier overview describes this leg in detail. -
Delivery on the destination chain. The destination chain's Axelar Gateway calls into the ITS edge contract, which decodes
RECEIVE_FROM_HUB(INTERCHAIN_TRANSFER), verifies that the original source chain is in its trusted chains list, and gives the recipient the tokens. The exact mechanism depends on the local token manager type (mint, unlock, and so on). If the original payment carried apayloadmemo, the ITS edge additionally calls an executable hook on the recipient contract.
Flow C: ITS transfer from a source chain to XRPL
A user on another Axelar connected chain wants to send value to a recipient on XRPL. In this flow the other chain is the source and XRPL is the destination. Because the XRPL ledger does not run arbitrary code, the destination side of the flow is the multisig account submitting a signed XRPL Payment to the recipient.
Sequence
sequenceDiagram
autonumber
actor User
participant Src as Source chain side
box LightYellow Axelar
participant Hub as ITS Hub
participant XGW as xrpl-gateway
participant XMP as xrpl-multisig-prover
participant MS as Multisig
participant XVV as xrpl-voting-verifier
end
participant Relayer as XRPL relayer
actor Verifiers
participant XRPL as XRPL ledger
actor Recipient
User->>Src: interchainTransfer to xrpl
Note over Src,Hub: standard inbound GMP leg<br/>source voting-verifier votes,<br/>router and Axelarnet Gateway route
Src->>Hub: deliver SEND_TO_HUB(InterchainTransfer)
Hub->>Hub: validate, increment XRPL supply,<br/>rescale, wrap as RECEIVE_FROM_HUB
Hub->>XGW: deliver via Axelarnet Gateway and router
XGW->>XGW: store outgoing message
XGW-->>Relayer: wasm-routing_outgoing event<br/>(delivered by the Axelar GMP API as a ConstructProofTask)
Relayer->>XMP: ConstructProof
XMP->>XGW: read outgoing message
XGW-->>XMP: message
XMP->>XMP: allocate ticket*, build XRPL Payment tx
XMP->>MS: StartSigningSession
MS-->>Verifiers: emit signing_started
Verifiers->>MS: SubmitSignature
Note over MS: quorum reached
MS-->>Relayer: wasm-signing_completed event<br/>(delivered by the Axelar GMP API as a GatewayTxTask, with the signed XRPL tx blob)
Relayer->>XRPL: submit signed Payment
XRPL->>Recipient: native XRP or IOU delivered
Relayer->>XRPL: observe outbound multisig tx
XRPL-->>Relayer: tx confirmed in ledger
Relayer->>XGW: VerifyMessages with ProverMessage
XGW->>XVV: VerifyMessages
XVV-->>Verifiers: emit messages_poll_started
Verifiers->>XVV: Vote
Note over XVV: quorum reached
XVV-->>Relayer: wasm-quorum_reached event (delivered by the Axelar GMP API)
Relayer->>XMP: ConfirmProverMessage
XMP->>XMP: release ticket, clear payload
* For background on why XRPL transactions use tickets and how the pool is managed, see Ticket in the glossary.
General steps
-
User call on the source chain. The user invokes the ITS edge contract on their source chain with an interchain transfer call whose destination chain is
xrpland whose destination address is the recipient's classic XRPL address encoded as raw bytes. The local token manager burns or locks the user's tokens, depending on the manager type configured for that token id. -
First GMP leg leaves the source chain. The source ITS edge wraps an
INTERCHAIN_TRANSFERpayload in aSEND_TO_HUBenvelope and callscallContract("axelar", ITS Hub address, payload)on the source chain's Axelar gateway. Verifiers observe the resulting on chain event, the voting verifier on the source chain side reaches quorum, the router routes the message into the Axelarnet Gateway, and the Axelarnet Gateway calls Execute on the ITS Hub. This is the same machinery used for any source chain. See the upstream Amplifier overview, the genericgateway, and the genericmultisig-proverfor context. -
ITS Hub processing. The Hub validates the source ITS edge, increments the registered supply for the XRPL token instance, rescales the amount into XRPL's six decimals (native XRP) or the IOU exponent format, checks neither side is frozen, and re wraps the inner message as
HubMessage::ReceiveFromHub(InterchainTransfer). It then callsCallContractagain, addressed to thexrpl-gatewayon Axelar. -
Outgoing message lands on the xrpl-gateway. The router delivers the message to the
xrpl-gateway. The gateway stores it in itsOUTGOING_MESSAGESmap keyed bycc_id. From this point the message is queued for inclusion in the next XRPL transaction the multisig prover builds. -
Prover constructs an XRPL Payment. Storing the outgoing message causes the
xrpl-gatewayto emit awasm-routing_outgoingevent. The Axelar GMP API picks the event up and pushes aConstructProofTaskto the relayer, which responds by callingConstructProof(cc_id, payload)on thexrpl-multisig-prover. The prover fetches the message from the gateway, decodesReceiveFromHub(InterchainTransfer), checks that the innerdatafield is empty (general message passing into XRPL is not supported, since XRPL has no executable destination), parses the destination as a classic XRPL address, converts the amount into the appropriate XRPL payment value (native drops or an IOU with a 54 bit mantissa and exponent — see Supported Values and Tokens on XRPL for the full conversion rules and constraints), allocates a ticket from its available ticket pool, and builds an XRPL Payment transaction whose sequence field is the allocated ticket. -
Signing session opens. The prover calls
StartSigningSessionon the genericmultisigcontract. The blob the signers must sign is the XRPL canonical serialization of the unsigned transaction. Each verifier signs a slightly different digest, because XRPL multi signing requires each signer's own XRPL account address to be appended to the canonical blob before hashing with SHA 512 half. Verifiers run the XRPL multisig handler under ampd. They read the signing session event, compute the per signer digest, hand it to tofnd for signing, and submit the resulting signature back to the multisig contract. Quorum follows the rules inservice-registry. -
Submission to XRPL. Once the multisig session reaches quorum, the generic
multisigcontract emits awasm-signing_completedevent. The Axelar GMP API picks it up, assembles the collected signatures into the final multi signed XRPL transaction blob, and pushes aGatewayTxTaskcontaining that blob to the relayer's includer. The includer submits the blob to the XRPL ledger as aPaymentfrom the multisig account to the recipient. For native XRP this delivers drops directly. For a remote origin IOU (a token whose XRPL representation is issued by the multisig account itself), the recipient must already have aTrustSetopen against the multisig account for the relevant currency code, otherwise XRPL rejects the payment. -
Confirmation of delivery on Axelar. Confirmation of an outbound XRPL transaction is implicit. The relayer's subscriber sees the multisig account's own outbound payment when it next polls, and the relayer reports it back to Axelar by calling
VerifyMessageson thexrpl-gatewaywith aProverMessagevariant. The gateway forwards it to thexrpl-voting-verifierwhich opens a poll on this new message; verifiers confirm that the transaction landed on XRPL with the expected unsigned tx hash. Once quorum is reached, the voting verifier emits awasm-quorum_reachedevent; the Axelar GMP API pushes the corresponding task to the relayer, which responds by callingConfirmProverMessageon thexrpl-multisig-prover. The ticket used by the transaction is released back into the available pool, and the payload is cleared. If the verifiers reportFailedOnChaininstead ofSucceeded, the ticket is still consumed, but the payload stays stored so it can be retried or the value refunded.
For unfamiliar terms used in this document, see the glossary.
Supported Values and Tokens on XRPL
This page explains everything a developer or integrator needs to know about which tokens the bridge supports, how amounts are represented on XRPL, how amounts get converted when bridging in either direction, and what XRPL-side constraints (trust lines, reserves, currency codes) you have to satisfy. The relevant source lives in packages/xrpl-types, the XRPL Gateway, and the XRPL Multisig Prover.
XRP vs IOU: the two value primitives
XRPL has exactly two primitive value types:
- Native XRP, the chain's native currency. Always denominated in drops as a 64-bit unsigned integer. 1 XRP = 106 drops, so XRP behaves as a 6-decimal token from the bridge's point of view.
- IOU (issued currency), XRPL's primitive for everything that is not XRP. An IOU is uniquely identified by the pair
(currency_code, issuer_account). Holders track their balance through a trust line opened against the issuer.
The bridge exposes both as cross-chain assets. They share the same on-chain message envelope (the Amount field of an XRPL Payment), but their internal representation and the conversion rules between an XRPL amount and an Axelar Uint256 are different.
In the Rust types these are unified by a single enum (packages/xrpl-types/src/types.rs):
pub enum XRPLPaymentAmount {
Drops(u64), // native XRP, in drops
Issued(XRPLToken, XRPLTokenAmount), // IOU: token id + a mantissa/exponent amount
}
pub struct XRPLToken {
pub issuer: XRPLAccountId,
pub currency: XRPLCurrency,
}
pub struct XRPLTokenAmount {
mantissa: u64,
exponent: i64,
}
XRP: native amounts in drops
XRP amounts are 64-bit unsigned integers of drops. There is a protocol-level cap on the maximum drops in circulation (and therefore on any single transfer):
| Constant | Value | Source |
|---|---|---|
XRP_DECIMALS | 6 | packages/xrpl-types/src/types.rs |
XRP_MAX_UINT | 100_000_000_000_000_000 (= 1017 drops, 100 billion XRP) | same |
When the bridge converts an amount coming from a smart-contract chain (delivered as Uint256 by the ITS Hub) into an XRPL drops amount, the multisig prover's compute_xrpl_amount checks source_amount <= XRP_MAX_UINT and otherwise returns InvalidTransferAmount. Any value above the cap is rejected at proof construction time.
IOU: 54-bit mantissa, 8-bit exponent
XRPL's IOU amounts are a custom floating-point format, documented here. The bridge stores them as (mantissa: u64, exponent: i64) and serializes them into XRPL's wire format with the following constraints (packages/xrpl-types/src/types.rs):
| Constant | Value |
|---|---|
MIN_MANTISSA | 1_000_000_000_000_000 (= 1015) |
MAX_MANTISSA | 10_000_000_000_000_000 - 1 (= 1016 - 1) |
MIN_EXPONENT | -96 |
MAX_EXPONENT | 80 |
XRPL_ISSUED_TOKEN_DECIMALS | 15 (the assumed decimals for the canonical Uint256 → IOU conversion) |
The on-wire binary layout for a non-zero IOU amount is:
1 bit : 1 ("not XRP")
1 bit : sign (always 1, positive)
8 bits : exponent + 97
54 bits: mantissa
(The mantissa is always normalized to MIN_MANTISSA <= mantissa <= MAX_MANTISSA, so it fits in 54 bits even though it spans roughly 15 to 16 decimal digits.)
For a zero amount XRPL uses the special encoding 0x8000000000000000.
The bridge never produces non-positive or non-canonical IOU amounts: zeros pass through as the canonical zero encoding, and any amount that would normalize outside the [MIN_MANTISSA, MAX_MANTISSA] window after exponent adjustment is rejected.
Canonicalization: turning a Uint256 into an XRPL IOU amount
When the XRPL Multisig Prover is delivering tokens to XRPL (Flow C), the ITS Hub hands it an integer amount in the source chain's decimals as a Uint256. The prover converts it to an XRPLTokenAmount via canonicalize_token_amount(amount: Uint256, decimals: u8):
- Start with the raw integer mantissa and an exponent of
-decimals(so the represented value ismantissa * 10^(-decimals)). - While
mantissa < MIN_MANTISSAandexponent > MIN_EXPONENT, multiply by 10 and subtract 1 from the exponent. - While
mantissa > MAX_MANTISSA, divide by 10 and add 1 to the exponent (overflow ifexponentwould exceedMAX_EXPONENT). - If
exponent < MIN_EXPONENT, the value rounds to 0 and the result is the canonical zero. - If
exponent > MAX_EXPONENTor the mantissa cannot be normalized into the window, the operation errors.
Source: canonicalize_mantissa and canonicalize_token_amount, both in packages/xrpl-types/src/types.rs.
For the reverse direction (XRPL inbound), the XRPL Gateway calls scale_to_decimals(amount: XRPLTokenAmount, destination_decimals: u8) to convert the IOU (mantissa, exponent) into a Uint256 aligned to whatever decimal precision the destination chain registered for the token with the ITS Hub. The same function returns zero if the input mantissa is zero, multiplies by 10^(exponent + destination_decimals) when the result is integer, and divides otherwise (silently flooring sub-precision dust).
Currency codes
XRPL currency codes are 160-bit values (20 bytes) but the wire format accepts them in two human-readable forms:
| Form | Regex | Notes |
|---|---|---|
| 3-char ASCII | ^[A-Za-z0-9\?\!@#\$%\^&\*<>\(\)\{\}\[\]|]{3}$ | The legacy XRPL form. The 3 characters are placed at bytes [12..15] of the 20-byte value; all other bytes are zero. |
| 40-char hex | ^[A-Fa-f0-9]{40}$ (excluding any string starting with 00) | The "non-standard" 160-bit currency code form. Used for codes longer than 3 characters or with characters outside the legacy ASCII set. |
Two values are explicitly reserved and rejected by XRPLCurrency::new:
- The literal ASCII
XRP(0x...58 52 50...), which would collide with native XRP at the wire-format level. - The all-zero 20-byte value (no currency).
The bridge enforces these as XRPLError::ReservedCurrency. Source: XRPLCurrency::is_reserved.
Token IDs
The bridge identifies each cross-chain token with the standard ITS 32-byte tokenId. On XRPL specifically the xrpl-gateway maintains three registries:
| Map | Purpose |
|---|---|
XRPL_TOKEN_TO_LOCAL_TOKEN_ID: Map<XRPLToken, TokenId> | Lookups for XRPL-native IOUs: the issuer is some XRPL account other than the multisig account, and the token has been registered via RegisterLocalToken. |
XRPL_CURRENCY_TO_REMOTE_TOKEN_ID: Map<XRPLCurrency, TokenId> | Lookups for remote-origin tokens: the XRPL representation is an IOU issued by the multisig account itself, registered via RegisterRemoteToken. |
TOKEN_ID_TO_XRPL_TOKEN: Map<TokenId, XRPLToken> | Reverse lookup. |
Native XRP has its own special token id computed once at gateway instantiation: tokenId(salt = keccak256("XRP" zero-padded to 20 bytes)) with deployer rrrrrrrrrrrrrrrrrrrrrhoLvTp (XRPL's canonical null-account). This is exposed via the gateway's XrpTokenId query.
Supported token categories
Across the whole bridge, three categories of tokens can move to and from XRPL:
| Category | XRPL representation | How a user receives it on XRPL | How tokens are "minted" on XRPL outbound delivery |
|---|---|---|---|
| Native XRP | Drops | Drops sent by the multisig account | Multisig account pays drops out of its own balance (sourced from inbound transfers) |
| XRPL-native IOU (origin = XRPL, issuer ≠ multisig) | IOU (currency, issuer) | Multisig account sends IOU it received on inbound transfers; multisig must have a TrustSet to the issuer. | Held in the multisig account's balance, sent out via Payment of that IOU |
| Remote-origin token (origin = another chain) | IOU (currency, multisig_issuer) | Multisig account mints the IOU to the recipient. Recipient must already have a TrustSet against the multisig account for that currency code. | New IOU created on the fly; no external balance needed |
Trust line setup
XRPL holders track IOU balances via trust lines. Two scenarios apply:
- For XRPL-native IOUs (issuer is some external XRPL account), the multisig account itself must have a
TrustSetopen against the issuer before it can hold the IOU and pay it back out. Operators trigger this by callingTrustSet { token_id }on the XRPL Multisig Prover, which builds and signs an XRPLTrustSettransaction. - For remote-origin tokens (issuer is the multisig account), the recipient on XRPL must have a
TrustSetopen against the multisig account before any inbound delivery can succeed. Without it the XRPL ledger rejects thePayment. This is a user-side requirement; the bridge does not open trust lines on the recipient's behalf.
Each trust line is also an "owned object" on the XRPL ledger and therefore consumes an owner reserve on the holder.
Address formats
XRPL accounts are referenced two ways in the bridge:
- In the on-XRPL world, as classic addresses: base58 strings starting with
r, for examplerPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY. Underneath this is a 20-byte account id (RIPEMD160(SHA256(public_key))). - Inside the on-Axelar contracts, as the type-safe
XRPLAccountId(20 bytes). Serialization to/from the classic-address string form is handled by thexrpl_account_id_stringserde module.
When a non-XRPL chain wants to send tokens to an XRPL recipient (Flow C), it uses the recipient's classic-address bytes encoded as the ITS destination_address. The XRPL Multisig Prover parses the bytes back into an XRPLAccountId at proof construction time and rejects malformed addresses.
Decimal handling across chains
The ITS Hub tracks per-chain decimals for every token in the TokenInstance map. Each chain registers its native decimals when the token is first deployed or linked there:
| Chain category | Decimals registered with the ITS Hub | Notes |
|---|---|---|
| XRPL (XRP) | 6 | Matches the drops representation. |
| XRPL (IOU) | 15 | The XRPL_ISSUED_TOKEN_DECIMALS constant. Used as the assumed precision for the Uint256 ↔ mantissa/exponent conversion. |
| Smart-contract chains | Whatever each chain registers (e.g., 18 for most EVM ERC-20s, 6 for USDC variants, 9 for Solana SPL, …) | Defined per token, per chain. |
When a transfer crosses a decimal boundary, the ITS Hub rescales the integer amount before forwarding. For XRPL the actual canonicalization into the mantissa/exponent form (or the truncation into drops) happens at the XRPL Multisig Prover, using the per-chain decimals it queries from the gateway. The reverse direction (XRPL outbound, Flows A and B) is symmetric: the XRPL Gateway queries the destination chain's decimals via the ITS Hub before scaling and wrapping the message.
Tickets
XRPL enforces a strict per-account Sequence number on every transaction (the same role an EVM nonce plays). For the multisig account that backs the bridge, this would normally mean only one transaction can be in flight at a time, and confirmations would have to land strictly in order. To get around that, the XRPL Multisig Prover uses tickets: pre-allocated sequence numbers that the multisig account reserves in advance, drawn from a pool of up to 250.
Tickets let many bridge transactions be signed and submitted in parallel, and let them confirm out of order. Operations that fundamentally have to be serial (pool refill, verifier set rotation, trust line management) still use a plain sequence number.
| Transaction type | Sequence field | Why |
|---|---|---|
Payment | Ticket | Many in flight at once; can confirm out of order |
TicketCreate | Plain sequence | Refills the ticket pool; must be strictly ordered |
SignerListSet | Plain sequence | Verifier set rotation; one at a time |
TrustSet | Plain sequence | One trust line at a time |
Ticket assignment
At any point a set of available tickets is tracked by the prover: ticket numbers that have been created with a TicketCreate transaction but that are not known to have been consumed by a transaction that made it to the XRPL ledger. New outbound transactions get assigned available tickets in order until the pool is exhausted.
If the number of available tickets falls below a configurable threshold, the prover constructs a new TicketCreate transaction (see Ticket creation flow below).
Ticket reuse under contention
If transactions are produced faster than the ticket pool can be refilled, ticket assignment wraps around to the first available ticket (similar to a round-robin scheduler). This can lead to multiple in-flight transactions being assigned the same ticket number:
tx8
This is safe because of how XRPL handles ticket consumption: only one of the transactions sharing a ticket will ultimately be included in the ledger. Once that happens, the ticket is consumed permanently by that single transaction; every other transaction that tried to use the same ticket will be rejected by the ledger.
Ticket creation flow
XRPL limits the number of available tickets at any given time to 250. When the size of the prover's ticket pool drops below a configurable threshold, a new TicketCreate transaction must be submitted to top it back up.
The flow:
- A
Paymenttransaction is confirmed on the XRPL ledger (and observed on Axelar through the standardProverMessageverification path), which reduces the number of available tickets below the creation threshold. - The relayer requests a new
TicketCreatetransaction from the XRPL Multisig Prover. - The prover serializes a
TicketCreatethat asks the ledger to create250 - len(available_tickets)new tickets. There may actually be fewer usable tickets on XRPL than the prover currently knows about (in-flight transactions that have not been confirmed yet on Axelar), but there can never be more, so this bound is safe. - Axelar verifiers sign the new
TicketCreatetransaction through the standard multi-sign session. - The relayer submits the signed transaction to XRPL.
- Once the transaction is included in a validated ledger, the relayer reports the inclusion back to Axelar as a
ProverMessage. - Axelar verifiers vote on whether the XRPL transaction has been finalized.
- If the poll succeeds, the newly created tickets become available to be assigned to subsequent
Paymenttransactions.
Fee reserve
In addition to the ticket pool, the prover tracks a FEE_RESERVE counter (in XRP drops) that represents the multisig account's available XRP for paying transaction fees. Every new transaction the prover builds is gated by ensure_sufficient_fee_reserve, which checks the budget against:
xrpl_base_reserve + xrpl_owner_reserve * (251 + trust_line_count) + tx_fee
The + 251 term accounts for the maximum 250 tickets plus the multisig account's own SignerList, each of which counts as an "owned object" on XRPL and contributes to the reserve requirement. The reserve is topped up via ConfirmAddReservesMessage after the XRPL Voting Verifier confirms an operator-initiated XRP top-up AddReservesMessage.
Firewalling and Flow Limits
The Axelar Amplifier protocol's hub-and-spoke topology is the basis of a security property called firewalling: a single compromised chain cannot exfiltrate more of a token from the bridged system than has legitimately been deposited into it. This page explains how the property is enforced and when it applies.
The hub-and-spoke topology
Every ITS-routed cross-chain transfer passes through the ITS Hub on Axelar. No two connected chains exchange tokens directly: a transfer from chain A to chain B is always two GMP legs glued together at the Hub:
A's ITS edge → axelarnet-gateway → ITS Hub → axelarnet-gateway → B's ITS edge
────── leg 1 ────── ────── leg 2 ──────
This shape means the Hub sees every interchain transfer in the ecosystem, exactly once. That central observation point is what lets the Hub enforce a global invariant per (chain, tokenId).
The Hub supply invariant
Per (chain, tokenId) the Hub stores a TokenInstance (contracts/interchain-token-service/src/state.rs):
#![allow(unused)] fn main() { pub struct TokenInstance { pub supply: TokenSupply, pub decimals: u8, } pub enum TokenSupply { /// The total token supply bridged to this chain. /// ITS Hub will not allow bridging back more than this amount of the /// token from the corresponding chain. Tracked(Uint256), Untracked, } }
On every InterchainTransfer the Hub runs apply_to_transfer:
#![allow(unused)] fn main() { interceptors::subtract_supply_amount(storage, &source_chain, &transfer)?; let transfer = interceptors::apply_scaling_factor_to_amount(...)?; interceptors::add_supply_amount(storage, &destination_chain, &transfer)?; }
The subtract_supply_amount interceptor calls supply.checked_sub(amount) on the source chain's TokenInstance. If Tracked and the subtraction would underflow, the Hub raises:
#![allow(unused)] fn main() { #[error("token supply invariant violated for token {token_id} on chain {chain}")] TokenSupplyInvariantViolated { token_id: TokenId, chain: ChainNameRaw }, }
and the entire transfer reverts. If Untracked, the subtraction is a no-op.
The invariant maintained per (chain, token) when Tracked is therefore:
Total bridged INTO the chain − Total bridged OUT of the chain ≥ 0
Equivalently: the Hub will not let more of a token leave a chain via ITS than has been bridged in via ITS. This is firewalling.
Why this protects against a single-chain failure
Suppose a chain's ITS edge is compromised, or a relayer crafts a fraudulent inbound report. The bad message reaches the Hub from that chain claiming a large outbound transfer (i.e., trying to "pull" tokens out of the bad chain to elsewhere). The Hub looks up the bad chain's TokenInstance.supply, attempts checked_sub(fraud_amount) from it, finds the counter does not cover it, and reverts. No tokens move. The fraud is contained to the compromised chain; the broader bridged system is untouched.
This is the property meant by "firewalling": one chain's failure does not become every chain's failure. The Hub is the chokepoint that enforces local conservation.
Edge Chain Flow Limits
Each edge chain's ITS provides the ability to configure per-window flow limits ("X tokens per Y hours") on every token it bridges. Since XRPL has no smart contracts, it cannot host this property. Its counterparts on the other side of the bridge can, though: in the XRPL to XRPL-EVM round-trip example, where XRP is bridged between the two chains, the XRPL-EVM edge can throttle the amount of XRP that flows in either direction within a 6-hour window, because XRPL-EVM is an EVM chain and runs the standard Solidity ITS.
How a limit is set on the XRPL-EVM edge
On EVM chains, the flow limit is configured by calling setFlowLimits on the InterchainTokenService contract:
// InterchainTokenService.sol (Solidity ITS edge)
/**
* @notice Used to set a flow limit for a token manager that has the service as its operator.
* @param tokenIds An array of the tokenIds of the tokenManagers to set the flow limits of.
* @param flowLimits The flowLimits to set.
*/
function setFlowLimits(bytes32[] calldata tokenIds, uint256[] calldata flowLimits)
external
onlyRole(uint8(Roles.OPERATOR))
{
uint256 length = tokenIds.length;
if (length != flowLimits.length) revert LengthMismatch();
for (uint256 i; i < length; ++i) {
deployedTokenManager(tokenIds[i]).setFlowLimit(flowLimits[i]);
}
}
The call takes parallel arrays of tokenIds and flowLimits and forwards each one to the corresponding TokenManager. Internally, each TokenManager.setFlowLimit writes the new cap to its own storage slot, and from that point on, every inbound or outbound transfer is gated against the new value.
Service Registry
The service registry keeps track of the pool of verifiers that vote and sign for each chain. The core functionalities, such as registering a new service, verifier authorization and un-authorization, can only be called from a governance address. Verifier bonding and unbonding, as well as registering support for specific chains, are called by the verifier themselves. The service registry is used by ampd, the voting verifier, and the multisig prover.
The term service refers to an upper-level entity that includes several chains. The difference is in how they are
related to each other, which is hierarchical: a service is like an umbrella that regulates the activities of several
chains that fall under its purview. The service defines common parameters, such as verifier requirements, bonding
details, and unbonding periods, which are applicable to all associated chains. Thus, a single instance of service
registry is used to organize and coordinate activities across all chains.
Interface
pub enum ExecuteMsg {
// Can only be called by governance account.
RegisterService {
service_name: String,
coordinator_contract: String,
min_num_verifiers: u16,
max_num_verifiers: Option<u16>,
min_verifier_bond: nonempty::Uint128,
bond_denom: String,
unbonding_period_days: u16,
description: String,
},
// Updates modifiable fields of the service. Permission: Governance.
UpdateService {
service_name: String,
updated_service_params: UpdatedServiceParams,
},
// Authorizes verifiers to join a service. Verifiers must still bond sufficient stake
// to participate. Permission: Governance.
AuthorizeVerifiers {
verifiers: Vec<String>,
service_name: String,
},
// Revoke authorization for specified verifiers. Verifiers bond remains unchanged.
// Permission: Governance.
UnauthorizeVerifiers {
verifiers: Vec<String>,
service_name: String,
},
// Jail verifiers. Jailed verifiers are not allowed to unbond or claim stake.
// Permission: Governance.
JailVerifiers {
verifiers: Vec<String>,
service_name: String,
},
// Register support for the specified chains. Permission: Specific(verifier).
RegisterChainSupport {
service_name: String,
chains: Vec<ChainName>,
},
// Deregister support for the specified chains. Permission: Specific(verifier).
DeregisterChainSupport {
service_name: String,
chains: Vec<ChainName>,
},
// Locks up any funds sent with the message as stake. Marks the sender as a potential
// verifier that can be authorized. Permission: Any.
BondVerifier { service_name: String },
// Initiates unbonding of staked funds for the sender. Permission: Any.
UnbondVerifier { service_name: String },
// Claim previously staked funds that have finished unbonding for the sender. Permission: Any.
ClaimStake { service_name: String },
}
pub enum QueryMsg {
// Returns the weighted active verifier list for the given service and chain.
ActiveVerifiers {
service_name: String,
chain_name: ChainName,
},
// Returns the Service config for the given service name.
Service { service_name: String },
// Returns details (registration, weight, supported chains) for a specific verifier.
Verifier {
service_name: String,
verifier: String,
},
}
// Fields that can be patched via UpdateService. Any None field leaves the existing value
// unchanged.
pub struct UpdatedServiceParams {
pub min_num_verifiers: Option<u16>,
pub max_num_verifiers: Option<Option<u16>>,
pub min_verifier_bond: Option<nonempty::Uint128>,
pub unbonding_period_days: Option<u16>,
}
Service Registry graph
flowchart TD
subgraph Axelar
Vr{"Voting Verifier"}
R{"Service Registry"}
end
OC{"Verifiers"}
Vr -- "ActiveVerifiers" --> R
OC -- "De/RegisterChainSupport" --> R
OC -- "Un/BondVerifier" --> R
OC -- "ClaimStake" --> R
Service Registry sequence diagram
sequenceDiagram
autonumber
box LightYellow Axelar
participant Service Registry
end
actor Governance
actor Verifier
Governance->>+Service Registry: Register Service
Governance->>+Service Registry: Authorize Verifiers
Verifier->>+Service Registry: Bond Verifier
Verifier->>+Service Registry: Register Chain Support
- Governance registers a new service by providing the necessary parameters for the service. It can later modify these
parameters via
UpdateService. - Governance authorizes verifiers to join the service by sending an
AuthorizeVerifiersmessage. - Verifiers bond to the service, providing stake, by sending a
BondVerifiermessage with appropriate funds included. Note that authorizing and bonding can be done in any order. - Verifiers register support for specific chains within the service by specifying service name and chain names.
Notes
- For the process of signing, verifiers need to register their public key in advance to be able to participate; the details of which are available in the multisig documentation.
Router Contract
The router contract is responsible for routing messages to and from registered gateways, as well as handling chain
registration, gateway upgrades, chain freezing, and an emergency global routing kill switch.
Governance registers chains and upgrades gateway addresses. The router admin or governance can freeze chains and
toggle the global routing kill switch.
Interface
pub enum ExecuteMsg {
// Registers a new chain with the router. Permission: Governance.
RegisterChain {
chain: ChainName,
gateway_address: Address,
msg_id_format: MessageIdFormat,
},
// Changes the gateway address associated with a particular chain. Permission: Governance.
UpgradeGateway {
chain: ChainName,
contract_address: Address,
},
// Freezes the specified chains in the specified directions. Permission: Elevated (admin or governance).
FreezeChains {
chains: HashMap<ChainName, GatewayDirection>,
},
// Unfreezes the specified chains in the specified directions. Permission: Elevated.
UnfreezeChains {
chains: HashMap<ChainName, GatewayDirection>,
},
// Emergency command to stop all amplifier routing. Permission: Elevated.
DisableRouting,
// Resumes routing after an emergency shutdown. Permission: Elevated.
EnableRouting,
// Routes each message to the gateway registered to the destination chain.
// Permission: Specific(gateway). Called by a registered gateway.
RouteMessages(Vec<Message>),
}
pub enum QueryMsg {
// Returns the ChainEndpoint for the given chain.
ChainInfo(ChainName),
// Returns a paginated list of chains registered with the router.
Chains {
start_after: Option<ChainName>,
limit: Option<u32>,
},
// Returns whether the global routing kill switch is currently enabled.
IsEnabled,
}
pub struct RouterInstantiated {
pub admin: Addr,
pub governance: Addr,
pub axelarnet_gateway: Addr,
}
pub struct ChainRegistered {
pub name: ChainName,
pub gateway: Addr,
}
pub struct GatewayInfo {
pub chain: ChainName,
pub gateway_address: Addr,
}
pub struct GatewayUpgraded {
pub gateway: GatewayInfo,
}
pub struct ChainFrozen {
pub name: ChainName,
pub direction: GatewayDirection,
}
pub struct ChainUnfrozen {
pub name: ChainName,
pub direction: GatewayDirection,
}
pub struct MessageRouted {
pub msg: Message,
}
pub struct RoutingDisabled;
pub struct RoutingEnabled;
Router graph
graph TD ea[External Gateway A] ra[Relayer A] subgraph Axelar ta[Gateway A] c[Router] tb[Gateway B] m[Multisig Prover] end rb[Relayer B] eb[External Gateway B] rc[Relayer C] ea--emit event-->ra ra--RouteMessages-->ta ta--RouteMessages-->c c--emit event-->rb rb--RouteMessages-->tb tb--get messages-->m m--construct proof-->rc rc--send proof-->eb
Message Routing sequence diagram
sequenceDiagram autonumber participant External Gateway A participant Relayer A box LightYellow Axelar participant Gateway A participant Router participant Gateway B participant Multisig Prover end participant Relayer B participant External Gateway B participant Relayer C External Gateway A->>+Relayer A: event emitted Relayer A->>+Gateway A: gateway::ExecuteMsg::RouteMessages Gateway A->>+Router: router::ExecuteMsg::RouteMessages Router-->>Relayer B: emit MessageRouted event Relayer B->>+Gateway B: gateway::ExecuteMsg::RouteMessages Multisig Prover->>+Relayer C: constructs proof Relayer C->>+External Gateway B: sends proof
- The External Gateway emits an event that is picked up by the Relayer.
- Relayer relays the event to the Gateway as a message.
- Gateway receives the incoming messages, verifies the messages, and then passes the messages to the Router.
- The Router emits a
MessageRoutedevent, which is picked up by Relayer B. The relayer then callsRouteMessageson Gateway B to forward the message. - The Multisig Prover takes the messages stored in the destination Gateway and constructs a proof.
- The Relayer sends the proof, which also contains messages, to the destination's External Gateway.
Notes
- External Gateways are deployed on blockchains other than Axelar, such as Ethereum and Avalanche, while internal gateways reside on Axelar's chain.
Multisig contract
This contract is used by prover contracts during proof construction to start a signing session and collect signatures from participants.
flowchart TD subgraph Axelar b[Prover] m[Multisig] end s[Verifier] b--StartSigningSession-->m s--SubmitSignature-->m b--Multisig (query)-->m
- StartSigningSession: The multisig contract receives a binary message from the prover. It uses the current active
verifier set to back a new signing session and then emits an event to notify signers that a message is pending
signature. The prover may optionally pass a
sig_verifieraddress that overrides the default per-signature check. - SubmitSignature: Each signer signs the message using their own private key and submits the signature. The contract validates that the signer is a participant in the verifier set associated with the session.
- Multisig (query): The prover queries this to retrieve the current state of the session, the collected signatures so far, and the verifier set with participant information. The final proof is assembled by the prover once the multisig session is completed.
Signing Sequence Diagram
sequenceDiagram participant Relayer box LightYellow Axelar participant Prover participant Multisig end actor Signers Relayer->>+Prover: ExecuteMsg::ConstructProof Prover->>+Multisig: ExecuteMsg::StartSigningSession Multisig-->>Signers: emit SigningStarted event Multisig->>-Prover: reply with session ID deactivate Prover loop Collect signatures Signers->>+Multisig: ExecuteMsg::SubmitSignature Multisig-->>Relayer: emit SignatureSubmitted event end Multisig-->>-Relayer: emit SigningCompleted event Relayer->>+Prover: QueryMsg::Proof Prover->>+Multisig: QueryMsg::Multisig Multisig-->>-Prover: reply with status, current signatures vector, and verifier set Prover-->>-Relayer: returns data and proof
Custom signature verification
If the multisig contract does not natively support the required signature verification, the sig_verifier parameter
in ExecuteMsg::StartSigningSession can be set by the prover to specify a custom signature verification contract. The
custom contract must implement the following interface defined in packages/signature-verifier-api:
pub enum QueryMsg {
#[returns(bool)]
VerifySignature {
signature: HexBinary,
message: HexBinary,
public_key: HexBinary,
signer_address: String,
session_id: Uint64,
},
}
When a custom verification contract is specified, on each SubmitSignature call the multisig contract will query the
custom contract's VerifySignature to validate the signature. The custom contract returns true if the signature is
valid or false otherwise.
sequenceDiagram actor Signers box LightYellow Axelar participant Multisig participant SignatureVerifier end Signers->>+Multisig: ExecuteMsg::SubmitSignature Multisig->>SignatureVerifier: QueryMsg::VerifySignature SignatureVerifier->>Multisig: returns true/false deactivate Multisig
Authorization
Prior to calling StartSigningSession, the prover contract must be authorized. To authorize one or more contracts,
governance calls AuthorizeCallers with a map of contract address to chain name. Authorization can be revoked by
either governance or the contract admin via UnauthorizeCallers. Each authorization is scoped to a single chain name:
a contract authorized for chain X can only start signing sessions for chain X.
Governance or the admin can also globally pause and resume signing via DisableSigning / EnableSigning.
Interface
pub enum ExecuteMsg {
// Open a new signing session. Permission: Specific(authorized) — only contracts that
// were previously approved via AuthorizeCallers can call this.
StartSigningSession {
verifier_set_id: String,
msg: HexBinary,
chain_name: ChainName,
// Optional address of a contract that overrides the default signature check.
// See "Custom signature verification" above.
sig_verifier: Option<String>,
},
// Submit a signature from a participant in the verifier set for the session.
// Permission: Any (the verifier set check is part of execution).
SubmitSignature {
session_id: Uint64,
signature: HexBinary,
},
// Register a new verifier set so future signing sessions can reference it by id.
// Permission: Any (callable by provers as part of their rotation flow).
RegisterVerifierSet { verifier_set: VerifierSet },
// Register a public key for the sender. To prevent registering someone else's key, the
// sender must sign their own address using the private key being registered.
// Permission: Any.
RegisterPublicKey {
public_key: PublicKey,
signed_sender_address: HexBinary,
},
// Authorize a set of contracts to call StartSigningSession. Each entry is scoped to a
// specific chain name. Permission: Governance.
AuthorizeCallers {
contracts: HashMap<String, ChainName>,
},
// Revoke authorization for a set of contracts. Permission: Elevated (admin or governance).
UnauthorizeCallers {
contracts: HashMap<String, ChainName>,
},
// Emergency command to stop all signing. Permission: Elevated.
DisableSigning,
// Resumes signing after an emergency shutdown. Permission: Elevated.
EnableSigning,
}
#[derive(QueryResponses)]
pub enum QueryMsg {
// Returns the Multisig session state, collected signatures, and verifier set.
#[returns(Multisig)]
Multisig { session_id: Uint64 },
// Returns a previously registered VerifierSet by its id.
#[returns(VerifierSet)]
VerifierSet { verifier_set_id: String },
// Returns a registered public key for a verifier and key type.
#[returns(PublicKey)]
PublicKey {
verifier_address: String,
key_type: KeyType,
},
// Returns whether the given contract is currently authorized to start signing sessions
// for the given chain.
#[returns(bool)]
IsCallerAuthorized {
contract_address: String,
chain_name: ChainName,
},
}
pub struct Multisig {
pub state: MultisigState,
pub verifier_set: VerifierSet,
pub signatures: HashMap<String, Signature>,
}
pub enum MultisigState {
Pending,
Completed { completed_at: u64 },
}
Events
pub enum Event {
// Emitted when a new signing session is open.
SigningStarted {
session_id: Uint64,
verifier_set_id: String,
pub_keys: HashMap<String, PublicKey>,
msg: MsgToSign,
chain_name: ChainName,
expires_at: u64,
},
// Emitted when a participant submits a signature.
SignatureSubmitted {
session_id: Uint64,
participant: Addr,
signature: Signature,
},
// Emitted when a signing session was completed.
SigningCompleted {
session_id: Uint64,
completed_at: u64,
chain_name: ChainName,
},
// Emitted when a PublicKey is registered.
PublicKeyRegistered {
verifier: Addr,
public_key: PublicKey,
},
// Emitted when a contract is authorized to create signing sessions for a chain.
CallerAuthorized {
contract_address: Addr,
chain_name: ChainName,
},
// Emitted when a contract is unauthorized.
CallerUnauthorized {
contract_address: Addr,
chain_name: ChainName,
},
// Emitted when signing is globally enabled.
SigningEnabled,
// Emitted when signing is globally disabled.
SigningDisabled,
}
Gateway
Not used on the XRPL side. XRPL traffic uses the XRPL Gateway, which absorbs the gateway role plus the ITS edge, the token-id registry, and gas accounting (responsibilities that on other chains live in separate contracts). The generic
gatewaycontract documented here is the template deployed for most other connected amplifier chains, so a cross-chain message from XRPL to, say, XRPL-EVM still passes through XRPL-EVM's generic gateway on the destination side.
The name gateway used in this documentation refers to those entities which reside
on axelar chain, which can also be called internal gateways. On the other hand we have
external gateways, which are gateways deployed on external chains connected to Axelar.
The gateway contract is how messages enter the amplifier protocol. Here are the steps taken throughout the lifecycle of a message:
- User sends a message to the external gateway. We call this an incoming message.
- Incoming messages are sent to the gateway via
VerifyMessages. - The gateway calls
VerifyMessageson the verifier, which submits the messages for verification (or just returns true if already verified). - The messages are verified asynchronously, and the verification status is stored in the verifier.
- Once the messages are verified,
RouteMessagesis called at the gateway, which forwards the verified messages to the router. - The router forwards each message to the gateway registered to the destination chain specified in the message.
- The prover retrieves the messages from the gateway, organizes them into a payload and submits the payload for signing.
- The relayer sends the signed payload to the external gateway.
Gateway graph
flowchart TD
subgraph Axelar
Sg{"Source Gateway"}
Dg{"Destination Gateway"}
Rt{"Router"}
end
Rl{"Relayer"}
Eg{"External Gateway"}
Rl -- "Listen for Message:M1 Event" --> Eg
Rl -- "M1" --> Sg
Sg -- "M1" --> Rt
Rt -- "M1" --> Dg
Interface
pub struct InstantiateMsg {
pub verifier_address: String,
pub router_address: String,
}
pub enum ExecuteMsg {
// Trigger verification at the linked voting verifier for any of the given messages
// that is still unverified. Permission: Any.
VerifyMessages(Vec<Message>),
// Forward the given messages to the next step of the routing layer. If the messages
// are coming in from an external chain, they must already be verified.
// Permission: Any.
RouteMessages(Vec<Message>),
}
pub enum QueryMsg {
// Messages stored for delivery to the chain corresponding to this gateway, queried
// by the multisig prover during proof construction.
OutgoingMessages(Vec<CrossChainId>),
}
The gateway only needs to know the address of the two contracts it works with: the voting verifier and the router.
Voting Verifier
Not used on the XRPL side. XRPL messages are verified by the XRPL Voting Verifier, which handles XRPL's union message type with five variants (
InterchainTransferMessage,CallContractMessage,AddGasMessage,AddReservesMessage,ProverMessage). The genericvoting-verifierdocumented here is the template deployed for most other connected amplifier chains.
The voting verifier verifies batches of messages via RPC voting. Polls are created and votes are cast via a generic voting module, which the voting verifier uses. The generic voting module does not know the meaning of the polls, and simply returns a Poll ID to the voting verifier. The voting verifier internally maps a Poll ID to the messages in the poll, storing the results in storage. The gateway can then query the voting verifier to check message verification status.
There are two types of polls: messages polls and verifier set polls. Messages polls are used to verify incoming
messages, while verifier set polls are used to verify that the external gateway has updated its stored verifier set.
Verifier set polls are a necessary component of the verifier set update flow. See update and confirm VerifierSet sequence diagram for more details.
Interface
pub enum ExecuteMsg {
// Computes the results of a poll. For all verified messages, marks them as verified
// in storage so the gateway can later query their status. Permission: Any.
EndPoll { poll_id: PollId },
// Casts votes for the specified poll. Permission: Any (only active verifiers count
// toward quorum).
Vote { poll_id: PollId, votes: Vec<Vote> },
// Returns a vector indicating current verification status for each message; starts a
// poll for any not yet verified messages. Permission: Any.
VerifyMessages(Vec<Message>),
// Starts a poll to confirm a verifier set update on the external gateway. Permission: Any.
VerifyVerifierSet {
message_id: nonempty::String,
new_verifier_set: VerifierSet,
},
// Update the threshold used for new polls. Permission: Governance.
UpdateVotingThreshold {
new_voting_threshold: MajorityThreshold,
},
}
pub enum QueryMsg {
// Returns a PollResponse for the given poll id.
Poll { poll_id: PollId },
// Returns the verification status of each message in the input.
MessagesStatus(Vec<Message>),
// Returns the verification status of the given verifier set.
VerifierSetStatus(VerifierSet),
// Returns the currently configured voting threshold.
CurrentThreshold,
}
Verifier graph
flowchart TD
subgraph Axelar
G{"Gateway"}
Vr{"Voting Verifier"}
R{"Service Registry"}
end
OC{"Verifiers"}
G--"VerifyMessages([M, M', M''])"-->Vr
Vr--"ActiveVerifiers"-->R
OC--"Vote(poll_id, votes)"-->Vr
OC--"EndPoll(poll_id)"-->Vr
Message Verification Sequence Diagram
sequenceDiagram
participant Gateway
participant Voting Verifier
participant Service Registry
participant OC as Verifiers
Gateway->>Voting Verifier: VerifyMessages([M,M',M''])
Voting Verifier->>Service Registry: ActiveVerifiers
Service Registry-->>Voting Verifier: list of verifiers and stake
Voting Verifier->>OC: emit event with poll_id and messages
Voting Verifier-->>Gateway: [false,false,false]
OC->>Voting Verifier: Vote(poll_id, votes)
OC->>Voting Verifier: Vote(poll_id, votes)
OC->>Voting Verifier: EndPoll(poll_id)
note right of Voting Verifier: Poll results are now stored.
opt After poll has ended
Gateway->>Voting Verifier: query MessagesStatus([M,M',M''])
Voting Verifier-->>Gateway: return [MessageStatus, ...]
end
Multisig prover contract
Not used on the XRPL side. XRPL outbound transactions are built by the XRPL Multisig Prover, which builds XRPL-native transactions (
Payment,SignerListSet,TicketCreate,TrustSet), manages tickets and the XRP fee reserve, and uses the XRPLSMT\0-prefixed SHA-512-half multi-signing digest. The genericmultisig-proverdocumented here is the template deployed for most other connected amplifier chains.
The prover contract is responsible for transforming gateway messages into a payload that is ready to be sent to the destination gateway. It calls the multisig contract to generate the signature proof and finally encodes both the data and proof so that relayers can take it and send it to the destination chain gateway.
Interface
pub enum ExecuteMsg {
// Start building a proof that includes specified messages. Permission: Any.
// Queries the gateway for actual message contents.
ConstructProof(Vec<CrossChainId>),
// Triggers a verifier set rotation if the registered set has diverged from the current
// one by more than `verifier_set_diff_threshold`. Permission: Elevated (admin or governance).
UpdateVerifierSet,
// Promotes a previously-prepared NextVerifierSet to be the current set, once a poll on
// the destination chain's SignersRotated event has confirmed it. Permission: Any.
ConfirmVerifierSet,
// Updates the signing threshold used for future verifier sets. The threshold currently
// in use does not change; the verifier set must be updated and confirmed for the change
// to take effect. Permission: Governance.
UpdateSigningThreshold {
new_signing_threshold: MajorityThreshold,
},
// Replaces the contract's admin address. Permission: Governance.
UpdateAdmin {
new_admin_address: String,
},
}
#[derive(QueryResponses)]
pub enum QueryMsg {
// Returns the current proof for a given multisig session, including the encoded
// execute_data once signing completes.
#[returns(ProofResponse)]
Proof { multisig_session_id: Uint64 },
// Returns the current active verifier set, or None if uninitialized.
#[returns(Option<VerifierSetResponse>)]
CurrentVerifierSet,
// Returns the next (pending) verifier set, or None if no rotation is in progress.
#[returns(Option<VerifierSetResponse>)]
NextVerifierSet,
}
pub enum ProofStatus {
Pending,
Completed { execute_data: HexBinary }, // encoded data and proof sent to destination gateway
}
pub struct ProofResponse {
pub multisig_session_id: Uint64,
pub message_ids: Vec<CrossChainId>,
pub payload: Payload,
pub status: ProofStatus,
}
pub struct VerifierSetResponse {
pub id: String,
pub verifier_set: multisig::verifier_set::VerifierSet,
}
Events
pub enum Event {
ProofUnderConstruction {
destination_chain: ChainName,
payload_id: PayloadId,
multisig_session_id: Uint64,
msg_ids: Vec<CrossChainId>,
},
}
Proof construction graph
graph TD r[Relayer] subgraph Axelar b[Prover] g[Gateway] m[Multisig] end s[Signer] r--ConstructProof-->b b--OutgoingMessages-->g g-.->b b--StartSigningSession-->m b--Multisig-->m s--SubmitSignature-->m
Proof construction sequence diagram
sequenceDiagram
autonumber
participant Relayer
box LightYellow Axelar
participant Prover
participant Gateway
participant Multisig
end
actor Signers
Relayer->>+Prover: ExecuteMsg::ConstructProof
alt payload not created previously
Prover->>+Gateway: QueryMsg::OutgoingMessages
Gateway-->>-Prover: query result
alt newer VerifierSet exists
Prover->>Prover: update next VerifierSet
end
else previously created payload found
Prover->>Prover: retrieves payload from storage
end
Prover->>+Multisig: ExecuteMsg::StartSigningSession
Multisig-->>Signers: emit SigningStarted event
Multisig->>-Prover: reply with session ID
Prover-->>Relayer: emit ProofUnderConstruction event
deactivate Prover
loop Collect signatures
Signers->>+Multisig: signature collection
end
Multisig-->>-Relayer: emit SigningCompleted event
Relayer->>+Prover: QueryMsg::Proof
Prover->>+Multisig: QueryMsg::Multisig
Multisig-->>-Prover: reply with status, current signatures vector and snapshot
Prover-->>-Relayer: returns ProofResponse
- Relayer asks Prover contract to construct proof providing a list of cross chain ids.
- If no payload for the given messages was previously created, it queries the gateway for the messages to construct it.
- With the retrieved messages, the Prover contract transforms them into a payload digest that needs to be signed by the multisig.
- If a previous payload was found for the given message ids, the Prover retrieves it from storage instead of querying the gateway and building it again.
- The Multisig contract is called asking to sign the payload digest.
- Multisig emits event
SigningStartedindicating a new multisig session has started. - Multisig triggers a reply in Prover returning the newly created session ID which is then stored with the payload for reference.
- Prover contract emits event
ProofUnderConstructionwhich includes the ID of the proof being constructed. - Signers submit their signatures until threshold is reached.
- Multisig emits event indicating the multisig session has been completed.
- Relayer queries Prover for the proof, using the multisig session ID.
- Prover queries Multisig for the multisig session.
- Multisig replies with the multisig state, the list of collected signatures so far and the snapshot of participants.
- If the Multisig state is
Completed, the Prover finalizes constructing the proof and returns theProofResponsestruct which includes the proof itself and the data to be sent to the destination gateway. If the state is not completed, the Prover returns theProofResponsestruct with thestatusfield set toPending.
Update and confirm VerifierSet graph
graph TD r[Relayer] subgraph Axelar b[Prover] v[Voting Verifier] m[Multisig] s[Service Registry] end r--UpdateVerifierSet-->b b--ActiveVerifiers-->s b--RegisterVerifierSet-->m r--ConfirmVerifierSet-->b b--VerifierSetStatus-->v
Update and confirm VerifierSet sequence diagram
sequenceDiagram autonumber participant External Gateway participant Relayer box LightYellow Axelar participant Prover participant Service Registry participant Voting Verifier participant Multisig end actor Verifier actor Signers Relayer->>+Prover: ExecuteMsg::UpdateVerifierSet alt existing VerifierSet stored Prover->>+Service Registry: QueryMsg::ActiveVerifiers Service Registry-->>-Prover: save new VerifierSet as next VerifierSet Prover->>+Multisig: ExecuteMsg::StartSigningSession (for rotate signers message) loop Collect signatures Signers->>+Multisig: signature collection end end Relayer->>+Prover: QueryMsg::Proof Prover-->>-Relayer: returns ProofResponse (new verifier set signed by old verifier set) Relayer-->>External Gateway: send new VerifierSet to the gateway, signed by old VerifierSet External Gateway-->>+Relayer: emit SignersRotated event Relayer->>+Voting Verifier: ExecuteMsg::VerifyVerifierSet Verifier->>+External Gateway: look up SignersRotated event, verify event matches verifier set in poll Verifier->>+Voting Verifier: ExecuteMsg::Vote Relayer->>+Voting Verifier: ExecuteMsg::EndPoll Relayer->>+Prover: ExecuteMsg::ConfirmVerifierSet Prover->>+Voting Verifier: QueryMsg::VerifierSetStatus Voting Verifier-->>-Prover: true Prover->>+Multisig: ExecuteMsg::RegisterVerifierSet
- The Relayer calls Prover to update the
VerifierSet. - The Prover calls Service Registry to get a
VerifierSet. - If a newer
VerifierSetwas found, the newVerifierSetis stored as the nextVerifierSet. The prover creates payload for the new verifier set. - The Multisig contract is called asking to sign the binary message.
- Signers submit their signatures until threshold is reached.
- Relayer queries Prover for the proof, using the multisig session id.
- If the Multisig state is
Completed, the Prover finalizes constructing the proof and returns theProofResponsestruct which includes the proof itself and the data to be sent to the External Chain's gateway. If the state is not completed, the Prover returns theProofResponsestruct with thestatusfield set toPending. - Relayer sends proof and data to the External Gateway.
- The gateway on the External Gateway processes the commands in the data and emits event
SignersRotated. - The event
SignersRotatedpicked up by the Relayer, the Relayer calls Voting Verifier to create a poll. - The Verifiers see the
PollStartedevent and look up theSignersRotatedevent on the External Gateway and verify the event matches the verifier set in the poll. - The Verifiers then vote on whether the event matches the verifiers or not.
- The Relayer calls the Voting Verifier to end the poll and emit
PollEndedevent. - Once the poll is completed, the Relayer calls the Prover to confirm if the
VerifierSetwas updated. - The Prover queries the Voting Verifier to check if the
VerifierSetis confirmed. - The Voting Verifier returns that the
VerifierSetis confirmed. - The Prover stores the
VerifierSetin itself and in Multisig.
Coordinator
Some contracts, like the multisig provers, are deployed per chain and unknown to one another. The coordinator contract keeps track of these chain-dependent provers and coordinates any interaction that requires knowledge of all of them. For example, the ability for a verifier to unbond their stake requires checking that it is not part of any active verifier set on any chain. Provers also push their currently active verifier set to the coordinator whenever it changes, so the coordinator can answer those cross-chain questions.
Interface
pub struct InstantiateMsg {
/// Governance address. Calls all governance-permissioned messages such as RegisterProverContract.
pub governance_address: String,
/// Service registry contract address on axelar. Used by VerifierInfo queries to resolve registration data.
pub service_registry: String,
}
pub enum ExecuteMsg {
// Registers a multisig prover address against a chain name. Used as part of the registry
// the coordinator maintains for cross-chain bookkeeping. Permission: Governance.
RegisterProverContract {
chain_name: ChainName,
new_prover_addr: String,
},
// Replaces the active verifier set the coordinator tracks for the calling prover.
// Provers should call this whenever their active set changes so the coordinator can
// answer ReadyToUnbond and VerifierInfo queries consistently across chains.
// Permission: Specific(prover) — only the prover registered for some chain can call.
SetActiveVerifiers { verifiers: HashSet<String> },
}
pub enum QueryMsg {
// Returns true if the verifier is not part of any active verifier set tracked by the
// coordinator. Used by the service registry as the unbonding gate.
ReadyToUnbond { verifier_address: String },
// Returns the verifier's registration entry, total weight, supported chains, and the
// set of chains the verifier is currently signing for.
VerifierInfo {
service_name: String,
verifier: String,
},
}
pub struct VerifierInfo {
pub verifier: Verifier,
pub weight: nonempty::Uint128,
pub supported_chains: Vec<ChainName>,
pub actively_signing_for: HashSet<Addr>,
}
Coordinator graph
flowchart TD
Co{"Coordinator"}
Go{"Governance"}
SR{"Service Registry"}
PrA{"Prover A"}
PrB{"Prover B"}
PrC{"Prover C"}
Go -- "RegisterProverContract" --> Co
PrA -- "SetActiveVerifiers" --> Co
PrB -- "SetActiveVerifiers" --> Co
PrC -- "SetActiveVerifiers" --> Co
SR -- "ReadyToUnbond" --> Co
SR -- "VerifierInfo" --> Co
XRPL Gateway
Source: contracts/xrpl-gateway.
The XRPL Gateway is the Axelar-side entry and exit point for all XRPL traffic. On other Amplifier chains a thin "gateway" contract simply moves verified messages between a voting verifier and the router; on XRPL the gateway is overloaded and also absorbs the ITS edge role, the token-id registry, and the gas accounting that on other chains lives in separate contracts. However, since XRPL has no smart contracts, every responsibility that would normally be on the external chain now lives on Axelar.
What XRPL-specific work this contract absorbs compared to the generic gateway
| Concern | Generic Gateway | XRPL Gateway |
|---|---|---|
| Verifying incoming messages with the voting verifier | Yes | Yes |
| Storing outgoing messages for the prover | Yes | Yes |
| Routing verified inbound to the router | Yes | Yes (RouteIncomingMessages) |
| Acting as ITS edge (translating to ITS Hub messages) | No (separate ITS edge contract on the chain) | Yes: holds the token-id registry, computes SEND_TO_HUB wrappers, queries Hub for destination decimals, scales amounts |
| Token registration / linking / metadata pushing | No | Yes: RegisterLocalToken, RegisterRemoteToken, RegisterTokenMetadata, LinkToken, DeployRemoteToken |
| Gas accounting for inbound messages | No | Yes: tracks accrued gas per token id; ConfirmAddGasMessages |
Interface
pub struct InstantiateMsg {
pub admin_address: String,
pub governance_address: String,
pub verifier_address: String, // the xrpl-voting-verifier
pub router_address: String,
pub its_hub_address: String, // ITS Hub contract on Axelar
pub its_hub_chain_name: ChainName,
pub chain_name: ChainName, // "xrpl"
pub xrpl_multisig_address: XRPLAccountId, // the on-XRPL gateway account
}
pub enum ExecuteMsg {
// ITS-edge token registration. Operations gated to admin/governance.
// Register XRPL token metadata for use in custom token linking. Permission: Elevated.
RegisterTokenMetadata { xrpl_token: XRPLTokenOrXrp },
// Register an XRPL-native token (issuer != multisig) as an interchain token.
// Derives tokenId = linked_token_id(chain_hash, issuer, currency_hash). Permission: Elevated.
RegisterLocalToken { xrpl_token: XRPLToken },
// Register a remote-origin token: maps an XRPL currency code to a token id whose XRPL
// representation is an IOU issued by the multisig itself. Permission: Elevated.
RegisterRemoteToken {
token_id: TokenId,
xrpl_currency: XRPLCurrency,
},
// Send a HubMessage::SendToHub(LinkToken) to the ITS Hub. Permission: Elevated.
LinkToken {
token_id: TokenId,
destination_chain: ChainNameRaw,
link_token: LinkToken,
},
// Send a HubMessage::SendToHub(DeployInterchainToken) to the ITS Hub. Permission: Elevated.
DeployRemoteToken {
xrpl_token: XRPLTokenOrXrp,
destination_chain: ChainNameRaw,
token_metadata: TokenMetadata,
},
// GMP machinery (called by relayers).
// Trigger verification at the voting verifier for any of the given XRPL messages that
// is still unverified. Permission: Any.
VerifyMessages(Vec<XRPLMessage>),
// Store outgoing messages received from the router (delivered to XRPL by the prover).
// Misnamed for router compatibility: this is "RouteOutgoingMessages" semantically.
// Permission: Specific(router).
RouteMessages(Vec<Message>),
// Route already-verified inbound messages on to the ITS Hub or to the destination
// chain (for pure GMP), depending on the XRPLMessage variant. Permission: Any.
RouteIncomingMessages(Vec<WithPayload<XRPLMessage>>),
// Credit a verified XRPLAddGasMessage to the accrued gas pool for its token id.
// Permission: Any.
ConfirmAddGasMessages(Vec<XRPLAddGasMessage>),
// Admin / killswitch.
UpdateAdmin { new_admin_address: String }, // Elevated
EnableExecution, // Elevated
DisableExecution, // Elevated
}
pub enum QueryMsg {
// Messages stored for delivery to XRPL (consumed by the multisig prover).
OutgoingMessages(Vec<CrossChainId>),
// Token-id registry lookups.
XrplToken(TokenId), // tokenId -> XRPLToken
XrplTokenId(XRPLToken), // XRPLToken -> tokenId
XrpTokenId, // tokenId for native XRP
LinkedTokenId(XRPLToken), // derive linked tokenId without persisting
// Per-chain decimals for a token, as registered with the ITS Hub.
TokenInstanceDecimals { chain_name: ChainNameRaw, token_id: TokenId },
// Translate an inbound XRPL message to the canonical ITS or GMP form (used by the
// relayer to preview a RouteIncomingMessages payload).
InterchainTransfer {
message: XRPLInterchainTransferMessage,
payload: Option<nonempty::HexBinary>,
},
CallContract {
message: XRPLCallContractMessage,
payload: nonempty::HexBinary,
},
IsEnabled,
}
XRPL message variants
The gateway accepts a single enum XRPLMessage (defined in upstream packages/xrpl-types) with five variants:
| Variant | Direction | Purpose |
|---|---|---|
InterchainTransferMessage | Inbound user payment | Token transfer through ITS to another chain (or to an executable on the destination if payload is set). |
CallContractMessage | Inbound user payment | Pure GMP call (no token transfer). Skips the ITS Hub entirely. |
AddGasMessage | Inbound user top-up | Adds gas to an in-flight cross-chain message identified by its original tx hash. |
AddReservesMessage | Inbound operator top-up | XRP-only payment to top up the multisig account's XRPL reserves. Confirmed by the multisig prover, not the gateway. |
ProverMessage | Outbound from the multisig | Reports that a prover-built XRPL transaction has landed on the ledger. Confirmed by the multisig prover. |
The gateway is the verification entry point for all five variants (via VerifyMessages), but only handles InterchainTransferMessage, CallContractMessage, and AddGasMessage in its routing/confirmation logic. AddReservesMessage and ProverMessage are confirmed by the XRPL Multisig Prover.
Routing graph: inbound XRPL message
sequenceDiagram autonumber participant Relayer box LightYellow Axelar participant XGW as xrpl-gateway participant XVV as xrpl-voting-verifier participant Hub as ITS Hub participant Router end Relayer->>XGW: VerifyMessages XGW->>XVV: VerifyMessages XVV-->>XGW: per-message status XGW-->>Relayer: response Note over XVV: poll runs and quorum is reached Relayer->>XGW: RouteIncomingMessages Note over XGW: InterchainTransferMessage path XGW->>Hub: query destination decimals Hub-->>XGW: decimals XGW->>XGW: scale amount and wrap as SEND_TO_HUB XGW->>Router: RouteMessages via Hub Note over XGW: CallContractMessage path XGW->>XGW: build plain Message, no Hub wrap XGW->>Router: RouteMessages direct GMP Relayer->>XGW: ConfirmAddGasMessages Note over XGW: AddGasMessage path XGW->>XGW: credit accrued gas for the token id
- The relayer submits an
XRPLMessagefor verification. - The gateway forwards to the XRPL Voting Verifier, which opens a poll.
- After quorum is reached the relayer triggers the appropriate follow-up entry point.
InterchainTransferMessageandCallContractMessageare routed viaRouteIncomingMessages;AddGasMessageis confirmed via the separateConfirmAddGasMessagesentry point. - For
InterchainTransferMessage, the gateway acts as the ITS edge: looks up the token id, queries the ITS Hub for destination decimals, scales the amount, wraps the inner ITS message asHubMessage::SendToHub(InterchainTransfer), and hands it to the router. - For
CallContractMessage, the gateway builds a plainrouter_api::Messagewith no Hub wrapping. The router delivers it straight to the destination chain's gateway as a pure GMP call. - For
AddGasMessage, the gateway credits the verified top-up amount toGAS_ACCRUED[token_id]; no router involvement.
Routing graph: outbound to XRPL
sequenceDiagram autonumber participant Router box LightYellow Axelar participant XGW as xrpl-gateway participant XMP as xrpl-multisig-prover end Router->>XGW: RouteMessages Note over XGW: gate: source_address must be the ITS Hub XGW->>XGW: store in OUTGOING_MESSAGES XMP->>XGW: query OutgoingMessages XGW-->>XMP: stored Messages
Messages arriving from the router via RouteMessages are not re-verified (the router is trusted), but the gateway rejects any message whose source_address is not the ITS Hub on Axelar. Only ITS-routed messages can be delivered to XRPL: pure GMP from another chain destined for XRPL is not supported. Accepted messages are stored in the OUTGOING_MESSAGES map keyed by cc_id. The XRPL Multisig Prover later reads them via the OutgoingMessages query when constructing an outbound XRPL Payment.
XRPL Voting Verifier
Source: contracts/xrpl-voting-verifier.
The XRPL Voting Verifier runs stake-weighted polls over XRPL transactions. Each poll asks the active verifier set whether a specific XRPL transaction happened on the XRPL ledger, what its outcome was, and whether its contents match the message under verification. Off chain, each verifier's ampd handler fetches the cited transaction over XRPL JSON RPC, applies XRPL-specific consistency checks, and casts a Vote. Quorum results are stored on chain and queried by the XRPL Gateway (or, for ProverMessage polls, indirectly used by the XRPL Multisig Prover) to decide what to do next.
What XRPL-specific work this contract absorbs compared to the generic voting verifier
| Concern | Generic Voting Verifier | XRPL Voting Verifier |
|---|---|---|
| Stake-weighted polls | Yes | Yes |
| Vote / EndPoll / VerifyMessages interface | Yes | Yes |
| Message type | Generic router_api::Message | XRPL-native XRPLMessage enum (five variants), so a single poll can cover inbound user payments, outbound prover transactions, gas top-ups, and reserve top-ups |
| Verifier set update polls | Separate VerifyVerifierSet execute variant | Not needed: verifier set rotation rides on a ProverMessage(SignerListSet) poll, no separate variant |
source_gateway_address field type | String | XRPLAccountId — type-safe XRPL classic address |
confirmation_height type | u64 | u32 (sufficient for XRPL ledger indexes) |
| Parameter updates | Multiple split-out execute variants in upstream; older UpdateVotingThreshold here | Unified UpdateVotingParameters with Option-typed fields for selective updates |
| Admin / killswitch | Not in the upstream version inherited here | Yes: UpdateAdmin, EnableExecution, DisableExecution |
Interface
pub struct InstantiateMsg {
pub admin_address: nonempty::String,
pub governance_address: nonempty::String,
pub service_registry_address: nonempty::String,
pub service_name: nonempty::String,
// The XRPL multisig classic address. Type-safe, distinct from a generic string.
pub source_gateway_address: XRPLAccountId,
pub voting_threshold: MajorityThreshold,
pub block_expiry: nonempty::Uint64,
pub confirmation_height: u32,
pub source_chain: ChainName,
}
pub enum ExecuteMsg {
// Compute the results of a poll. Anyone can call. Emits a PollEnded event.
// Permission: Any.
EndPoll { poll_id: PollId },
// Cast votes for the specified poll. Caller must be a participant in the snapshot.
// Permission: Any (the snapshot check happens during execution).
Vote { poll_id: PollId, votes: Vec<Vote> },
// Return a per-message verification status, opening a poll for any not-yet-verified
// messages. Called by the XRPL Gateway. Permission: Any.
VerifyMessages(Vec<XRPLMessage>),
// Update voting parameters. Each field is Option so callers can patch a subset.
// Changes apply to future polls only. Permission: Governance.
UpdateVotingParameters {
voting_threshold: Option<MajorityThreshold>,
block_expiry: Option<nonempty::Uint64>,
confirmation_height: Option<u32>,
},
// Killswitch and admin rotation. Permission: Elevated.
EnableExecution,
DisableExecution,
UpdateAdmin { new_admin_address: String },
}
pub enum QueryMsg {
// Returns the poll state, the messages it covers, and its status (in progress / finished).
Poll { poll_id: PollId },
// Returns the verification status of each input XRPLMessage.
MessagesStatus(Vec<XRPLMessage>),
// Returns the currently active voting threshold, block expiry, and confirmation height.
VotingParameters,
}
pub enum PollData {
Messages(Vec<XRPLMessage>),
}
pub struct PollResponse {
pub poll: WeightedPoll,
pub data: PollData,
pub status: PollStatus,
}
pub struct MessageStatus {
pub message: XRPLMessage,
pub status: VerificationStatus,
}
Verification sequence
sequenceDiagram
autonumber
participant XGW as xrpl-gateway
box LightYellow Axelar
participant XVV as xrpl-voting-verifier
participant SR as Service Registry
end
actor Verifiers
XGW->>XVV: VerifyMessages
XVV->>SR: ActiveVerifiers
SR-->>XVV: weighted snapshot
XVV-->>Verifiers: emit messages_poll_started
XVV-->>XGW: per-message status, initially false or in-progress
loop Verifiers vote
Verifiers->>XVV: tx RPC lookup on XRPL
Verifiers->>XVV: Vote
Note over XVV: emit wasm-quorum_reached when threshold crossed
end
Note over XVV: the wasm-quorum_reached event drives the relayer's next call,<br/>which depends on the XRPLMessage variant being verified
General steps
- The XRPL Gateway forwards an array of
XRPLMessages viaVerifyMessages. For any message that already has a final status (e.g.,SucceededOnSourceChain), no poll is opened; only unverified messages produce a new poll. - The voting verifier takes a weighted snapshot of the active verifiers for chain
xrplfrom the Service Registry. The snapshot is bound to the poll for its entire lifetime. - A
messages_poll_startedevent is emitted carrying the candidateXRPLMessages. Off chain, each verifier's XRPL handler picks the event up, fetches the cited XRPL transactions via JSON RPC, runs XRPL-specific consistency checks (multi-sign destination,delivered_amount == Amountto close thetfPartialPaymentexploit, memo parsing, payload hash), and casts aVote. - The moment a vote pushes weighted tally past quorum, the contract emits a
wasm-quorum_reachedevent. The Axelar GMP API delivers a task to the XRPL relayer. The relayer's next action depends on theXRPLMessagevariant: forInterchainTransferMessageorCallContractMessageit callsRouteIncomingMessageson the gateway; forProverMessageit callsConfirmProverMessageon the multisig prover. EndPollis called separately to finalize the poll status. It is not on the critical path for routing.
Why there's no VerifyVerifierSet variant
On the generic voting-verifier, verifier set rotations are confirmed by a separate VerifyVerifierSet execute variant that opens a different kind of poll. On XRPL the gateway is an XRPL account, not a contract, so the rotation event is the inclusion of a SignerListSet transaction on the XRPL ledger. That inclusion is reported back to Axelar as a ProverMessage (with the SignerListSet unsigned tx hash) and confirmed through the same VerifyMessages poll machinery used for any other XRPL transaction. The verifier set rotation is then promoted by the multisig prover's ConfirmProverMessage handler.
XRPL Multisig Prover
Source: contracts/xrpl-multisig-prover.
The XRPL Multisig Prover constructs fully-canonical XRPL transactions that the on-XRPL multisig account submits to deliver Axelar-to-XRPL traffic. On other Amplifier chains the prover only has to produce an execute_data blob that the destination chain's gateway will then verify and execute; on XRPL there is no destination contract to do that verification, so the prover's output is the on-chain transaction itself. The prover therefore also owns all the XRPL-specific bookkeeping that this implies: a pool of pre-allocated tickets for parallel out-of-order in-flight transactions, an XRP reserve budget for the multisig account, trust line management for receiving local IOUs, and the current/next verifier set view used to populate the multisig account's SignerList.
What XRPL-specific work this contract absorbs compared to the generic prover
| Concern | Generic Multisig Prover | XRPL Multisig Prover |
|---|---|---|
| ConstructProof / signing session lifecycle | Yes | Yes |
| UpdateVerifierSet / ConfirmVerifierSet | Yes (ConfirmVerifierSet from voting verifier) | Replaced by ConfirmProverMessage (verifier rotation rides a SignerListSet confirmation) |
| Output | Encoded calldata for destination chain's gateway | An XRPL canonical-serialized transaction (Payment / SignerListSet / TicketCreate / TrustSet) |
| Sequence management | N/A (no per-account nonce on Axelar side) | Yes: maintains AVAILABLE_TICKETS pool, NEXT_SEQUENCE_NUMBER, LAST_ASSIGNED_TICKET_NUMBER |
| Fee/reserve budget | N/A | Yes: FEE_RESERVE tracks the multisig account's XRP balance budget; ConfirmAddReservesMessage credits top-ups |
| Per-token plumbing | N/A | Yes: TrustSet execute variant for opening trust lines so the multisig account can receive a local-origin IOU |
| Proof status query | Returns full ProofResponse with execute_data | Returns unsigned_tx_hash plus Completed { execute_data: signed XRPL tx blob } |
Interface
pub struct InstantiateMsg {
pub admin_address: String,
pub governance_address: String,
pub gateway_address: String, // the xrpl-gateway
pub multisig_address: String, // the generic multisig contract
pub coordinator_address: String,
pub service_registry_address: String,
pub voting_verifier_address: String, // the xrpl-voting-verifier
pub signing_threshold: MajorityThreshold,
pub service_name: String,
pub chain_name: ChainName, // "xrpl"
pub verifier_set_diff_threshold: u32,
pub xrpl_multisig_address: XRPLAccountId,
// XRPL-specific fee + reserve configuration.
pub xrpl_transaction_fee: u64, // base fee per signer; final tx fee = fee * (1 + n_signers)
pub xrpl_base_reserve: u64, // protocol base reserve
pub xrpl_owner_reserve: u64, // per-owned-object reserve
pub initial_fee_reserve: u64, // bootstrap budget
// XRPL ticket pool bootstrap.
pub ticket_count_threshold: u32, // refill the pool when available count drops below this
pub available_tickets: Vec<u32>, // initial set of usable tickets
pub next_sequence_number: u32,
pub last_assigned_ticket_number: u32,
}
pub enum ExecuteMsg {
// Build an XRPL Payment for a single outgoing message and open a signing session.
// Permission: Any (the relayer calls this; gas is paid via accrued gas accounting).
ConstructProof {
cc_id: CrossChainId,
payload: HexBinary,
},
// Trigger a verifier set rotation: build an XRPL SignerListSet signed by the
// outgoing set. Permission: Elevated (admin or governance).
UpdateVerifierSet,
// Confirm that a prover-built XRPL transaction landed on the ledger.
// After the xrpl-voting-verifier confirms the corresponding ProverMessage poll, the
// relayer calls this to release the ticket, clear the payload, and (for SignerListSet)
// promote NextVerifierSet -> CurrentVerifierSet. Permission: Any.
ConfirmProverMessage { prover_message: XRPLProverMessage },
// Credit a verified XRPLAddReservesMessage to FEE_RESERVE. Permission: Any.
ConfirmAddReservesMessage { add_reserves_message: XRPLAddReservesMessage },
// Build an XRPL TicketCreate transaction. Permission: Any (the relayer calls when the
// pool drops below threshold; the contract decides internally how many to mint).
TicketCreate,
// Build an XRPL TrustSet transaction for the multisig account to be able to hold
// the given local-origin IOU. Permission: Elevated.
TrustSet { token_id: TokenId },
// Admin / parameter updates.
DisableExecution, // Elevated
EnableExecution, // Elevated
UpdateFeeReserve { new_fee_reserve: u64 }, // Elevated
UpdateSigningThreshold { // Governance
new_signing_threshold: MajorityThreshold,
},
UpdateXrplTransactionFee { new_transaction_fee: u64 }, // Elevated
UpdateXrplReserves { // Elevated
new_base_reserve: u64,
new_owner_reserve: u64,
},
UpdateAdmin { new_admin_address: String }, // Elevated
// Verifies a signature against an XRPL multi-sign digest (which includes the signer's
// own XRPL address). Used by the multisig contract during signature submission when
// sig_verifier is set. Permission: Any.
VerifySignature {
signature: HexBinary,
message: HexBinary,
public_key: HexBinary,
signer_address: String,
session_id: Uint64,
},
}
pub enum QueryMsg {
// Returns the unsigned tx hash and either ProofStatus::Pending or
// ProofStatus::Completed { execute_data: <signed XRPL tx blob> }.
Proof { multisig_session_id: Uint64 },
// Same XRPL-specific signature check as the execute variant, callable as a query.
VerifySignature { /* same args */ },
// The current active verifier set view (the one mirrored into the XRPL SignerList).
CurrentVerifierSet,
// The pending NextVerifierSet, if a rotation is in progress.
NextVerifierSet,
// Look up the in-flight multisig session for a specific cc_id.
MultisigSession { cc_id: CrossChainId },
// Recommends how many tickets the next TicketCreate should produce. 0 means no refill needed.
TicketCreate,
IsEnabled,
AvailableTickets,
NextSequenceNumber,
FeeReserve,
}
pub struct ProofResponse {
pub unsigned_tx_hash: HexTxHash,
pub status: ProofStatus,
}
pub enum ProofStatus {
Pending,
Completed { execute_data: HexBinary },
}
Proof construction sequence
sequenceDiagram
autonumber
participant Relayer
box LightYellow Axelar
participant XMP as xrpl-multisig-prover
participant XGW as xrpl-gateway
participant MS as Multisig
end
actor Signers
Relayer->>XMP: ConstructProof
XMP->>XGW: OutgoingMessages
XGW-->>XMP: stored router Message
XMP->>XMP: decode RECEIVE_FROM_HUB,<br/>allocate ticket from AVAILABLE_TICKETS,<br/>build XRPLPaymentTx canonical bytes
XMP->>MS: StartSigningSession with canonical bytes
MS-->>Signers: emit SigningStarted
MS-->>XMP: reply with session ID
XMP-->>Relayer: emit ProofUnderConstruction event
loop Collect signatures
Signers->>MS: SubmitSignature
end
MS-->>Relayer: emit SigningCompleted
Relayer->>XMP: Proof query with session ID
XMP->>MS: Multisig query
MS-->>XMP: state and signatures
XMP-->>Relayer: ProofResponse with signed XRPL tx blob
General steps
- The relayer calls
ConstructProof(cc_id, payload)after the ITS Hub has routed an inbound message into the XRPL Gateway'sOUTGOING_MESSAGESmap. - The prover queries the XRPL Gateway for the stored message, decodes
HubMessage::ReceiveFromHub(InterchainTransfer), and verifiesdatais empty (XRPL has no executable destination; the multisig prover rejects payloads). - It parses the destination as a classic XRPL address, converts the amount into the right
XRPLPaymentAmount(drops for XRP, or 54-bit-mantissa + exponent for an IOU), allocates a ticket fromAVAILABLE_TICKETS, and builds an XRPL Payment transaction whoseSequencefield is the allocated ticket. - The prover opens a signing session at the generic
multisigcontract, passing the XRPL canonical serialization as the message to sign. Each verifier signs a slightly different digest, because XRPL multi-signing requires appending the signer's own XRPL account id to the canonical bytes before the SHA-512-half hash. - Once quorum is reached, the relayer queries
Proof { multisig_session_id }and gets backProofStatus::Completed { execute_data: <signed XRPL tx blob> }. It then broadcasts the blob to the XRPL ledger. - The relayer's subscriber sees the multisig account's outbound transaction land on the ledger and reports it back via
VerifyMessageswith aProverMessage. Once the XRPL Voting Verifier confirms it, the relayer callsConfirmProverMessagewhich releases the ticket and clears the stored payload.
Update and confirm VerifierSet graph
graph TD
Relayer
subgraph Axelar
XMP[xrpl-multisig-prover]
SR[Service Registry]
XVV[xrpl-voting-verifier]
MS[Multisig]
Coord[Coordinator]
end
Relayer -- UpdateVerifierSet --> XMP
XMP -- ActiveVerifiers --> SR
XMP -- RegisterVerifierSet --> MS
XMP -- SetActiveVerifiers --> Coord
Relayer -- ConfirmProverMessage --> XMP
XMP -- MessagesStatus(ProverMessage) --> XVV
Update and confirm VerifierSet sequence
sequenceDiagram
autonumber
participant XRPL as XRPL ledger
participant Relayer
box LightYellow Axelar
participant XGW as xrpl-gateway
participant XMP as xrpl-multisig-prover
participant SR as Service Registry
participant MS as Multisig
participant XVV as xrpl-voting-verifier
end
actor Signers
Relayer->>XMP: UpdateVerifierSet
XMP->>SR: ActiveVerifiers
SR-->>XMP: candidate verifier set
Note over XMP: continue only if diff<br/>exceeds the configured threshold
XMP->>XMP: save as NextVerifierSet
XMP->>MS: StartSigningSession with SignerListSet bytes,<br/>signed by the current verifier set
loop Collect signatures
Signers->>MS: SubmitSignature
end
Relayer->>XMP: Proof query
XMP-->>Relayer: signed SignerListSet tx
Relayer->>XRPL: submit signed SignerListSet
XRPL-->>Relayer: tx confirmed in ledger
Relayer->>XGW: VerifyMessages with ProverMessage
Note over XVV: standard ProverMessage poll runs
Relayer->>XMP: ConfirmProverMessage
XMP->>XVV: MessagesStatus query
XVV-->>XMP: SucceededOnSourceChain
XMP->>XMP: promote NextVerifierSet to CurrentVerifierSet
XMP->>MS: RegisterVerifierSet
General steps
- The relayer (or governance) calls
UpdateVerifierSet. - The prover queries the Service Registry for the active verifier set for chain
xrpl. If the diff against the current set exceedsverifier_set_diff_threshold, the candidate becomes the prover'sNextVerifierSet. - The prover builds an XRPL
SignerListSettransaction that mirrors the new verifier set into the multisig account's on-XRPLSignerList, with weights scaled to fit XRPL'su16weight field, and a quorum derived from the prover'ssigning_threshold. The transaction is signed by the outgoing verifier set. - The relayer broadcasts the signed
SignerListSetto the XRPL ledger. The transaction inclusion is then reported back to Axelar as aProverMessageand confirmed through the standardVerifyMessagespoll machinery. ConfirmProverMessageis called after the poll reaches quorum. The prover promotesNextVerifierSettoCurrentVerifierSet, callsRegisterVerifierSeton the genericmultisigcontract so subsequent signing sessions reference the new set, and pushes the new set to the Coordinator viaSetActiveVerifiersfor cross-chain bookkeeping.
Why ConfirmProverMessage replaces the generic ConfirmVerifierSet
On the generic prover, after a rotateSigners calldata is broadcast to the destination chain, the destination's gateway emits a SignersRotated event which is verified by the destination chain's voting verifier. ConfirmVerifierSet reads that result and promotes the set. On XRPL there is no contract to emit an event: a SignerListSet transaction landing in the ledger is the event. The prover unifies this with the generic message-confirmation path by treating every prover-built outbound XRPL transaction (Payment, SignerListSet, TicketCreate, TrustSet) as a ProverMessage that the XRPL Voting Verifier polls on. ConfirmProverMessage then dispatches on the inner transaction type to release the ticket, promote the verifier set, refresh available tickets, or activate a trust line.
Tickets, sequence numbers, and fee reserve
See Tickets for the full treatment of how the prover manages XRPL sequence numbers, the ticket pool (assignment, reuse under contention, refill flow), and the FEE_RESERVE budget.
Access Control for Contract Messages
Each execute message in the Amplifier contracts is annotated with a permission scope. The vocabulary is defined by the
msgs-derive macro and a message can have any of the following:
Any: anyone can call it. Default for user-facing methods.Governance: only the configured governance address.Admin: only the configured admin address.Elevated: admin or governance.Specific(role): only an address that holds the named role. Common variants in this codebase:Specific(gateway),Specific(verifier),Specific(prover),Specific(authorized).Proxy(role): a single trusted contract may also call this on behalf of the named role. Used today forProxy(coordinator)so the coordinator can register chains/callers on behalf of governance.
Only execute messages with restricted access are listed below. Messages tagged Any are omitted.
Router
Governance
#![allow(unused)] fn main() { RegisterChain { chain: ChainName, gateway_address: Address, msg_id_format: MessageIdFormat, }, UpgradeGateway { chain: ChainName, contract_address: Address, }, }
Elevated
#![allow(unused)] fn main() { FreezeChains { chains: HashMap<ChainName, GatewayDirection>, }, UnfreezeChains { chains: HashMap<ChainName, GatewayDirection>, }, DisableRouting, EnableRouting, }
Specific(gateway)
Only a registered chain gateway may call this:
#![allow(unused)] fn main() { RouteMessages(Vec<Message>) }
Voting Verifier
Governance
#![allow(unused)] fn main() { UpdateVotingThreshold { new_voting_threshold: MajorityThreshold, } }
Multisig Prover
Governance
#![allow(unused)] fn main() { UpdateSigningThreshold { new_signing_threshold: MajorityThreshold, }, UpdateAdmin { new_admin_address: String, } }
Elevated
#![allow(unused)] fn main() { UpdateVerifierSet }
Multisig
Governance
#![allow(unused)] fn main() { AuthorizeCallers { contracts: HashMap<String, ChainName>, } }
Elevated
#![allow(unused)] fn main() { UnauthorizeCallers { contracts: HashMap<String, ChainName>, }, DisableSigning, EnableSigning, }
Specific(authorized)
Only a contract previously authorized via AuthorizeCallers may call this, and only for the chain name it was
authorized for:
#![allow(unused)] fn main() { StartSigningSession { verifier_set_id: String, msg: HexBinary, chain_name: ChainName, sig_verifier: Option<String>, } }
Service Registry
Governance
#![allow(unused)] fn main() { RegisterService { service_name: String, coordinator_contract: String, min_num_verifiers: u16, max_num_verifiers: Option<u16>, min_verifier_bond: nonempty::Uint128, bond_denom: String, unbonding_period_days: u16, description: String, }, UpdateService { service_name: String, updated_service_params: UpdatedServiceParams, }, AuthorizeVerifiers { verifiers: Vec<String>, service_name: String, }, UnauthorizeVerifiers { verifiers: Vec<String>, service_name: String, }, JailVerifiers { verifiers: Vec<String>, service_name: String, } }
Specific(verifier)
Called by the verifier address itself:
#![allow(unused)] fn main() { RegisterChainSupport { service_name: String, chains: Vec<ChainName>, }, DeregisterChainSupport { service_name: String, chains: Vec<ChainName>, } }
Coordinator
Governance
#![allow(unused)] fn main() { RegisterProverContract { chain_name: ChainName, new_prover_addr: String, } }
Specific(prover)
Only a prover address that was previously registered via RegisterProverContract may call this, and only with respect
to the chain it was registered for:
#![allow(unused)] fn main() { SetActiveVerifiers { verifiers: HashSet<String>, } }
Glossary
This page defines the XRPL and Axelar specific terms used in the rest of the book.
XRPL terms
account_tx
The XRPL JSON RPC method that returns a paginated list of transactions involving a given account. The XRPL relayer polls account_tx for the multisig account to discover new user payments and to detect inclusion of multisig prover outputs.
Canonical serialization
The deterministic XRPL byte format used when hashing a transaction for signing. Every field is sorted by its protocol defined type_code and field_code and encoded in a fixed wire format. Because XRPL nodes all agree on this serialization, any signer can independently produce the same bytes from the same transaction structure and arrive at the same digest. Multi signing makes this rule sensitive: each signer must append their own account address to the canonical bytes before hashing.
Classic address
The base58 encoded XRPL account identifier, starting with r (for example, rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY). It is the human readable form of a 20 byte account id derived from RIPEMD160(SHA256(public_key)).
Currency code
A 3 character ASCII code (such as USD) or a 40 character (20 byte) hex code identifying an IOU. The first form is reserved for common ASCII codes; the second form is required for any code that is not exactly 3 ASCII letters. The pair (currency, issuer) uniquely identifies an IOU on XRPL.
DisableMaster (Master Key)
Every XRPL account is created with a master key derived from its account seed. DisableMaster is an AccountSet flag that turns off the master key, leaving the account controllable only through the SignerList. The multisig account that acts as the bridge gateway has DisableMaster set, so no single party can move funds out of it.
Drop
The smallest unit of native XRP. One XRP equals 106 drops. All XRP amounts in XRPL transactions are denominated in drops as a u64 integer. XRP itself has 6 decimals from the bridge's point of view.
IOU (issued currency)
XRPL's primitive for any token that is not XRP. An IOU is identified by the pair (currency_code, issuer_account) and is created implicitly when its issuer makes a Payment of that currency to an account that has opened a trust line. The bridge uses IOUs to represent tokens that originate on other chains: the multisig account is the issuer and mints the IOU to the recipient.
Memos
A transaction field on XRPL that carries arbitrary byte data. Each memo entry holds three optional sub fields: MemoType, MemoData, and MemoFormat, each encoded as hex. The bridge uses memos as a structured key value store. The exact set of expected keys depends on the value of the type memo: an interchain_transfer or call_contract payment encodes destination_chain, destination_address, gas_fee_amount, and an optional payload; an add_gas payment encodes only msg_id; an add_reserves payment carries no further keys.
Multisig account
A regular XRPL account that has had a SignerListSet transaction applied to define a list of signers, each with a weight, plus a quorum threshold. Once configured this way (and with the master key disabled), the account can only send transactions that have been signed by enough listed signers to reach the quorum. The bridge gateway is exactly such an account; the signer list mirrors the active Axelar verifier set.
Payment
The XRPL transaction type used to move value, either native XRP between accounts or an IOU between accounts that share a trust line. The bridge uses Payment both inbound (the user paying the multisig account) and outbound (the multisig account paying the recipient).
Reserve
A minimum XRP balance every XRPL account must keep in order to exist. There is a fixed base reserve per account, plus an owner reserve per owned object (each ticket, each trust line, each signer list entry, and so on). The multisig account's reserve is the dominant variable cost of running the bridge over time; it grows with the number of trust lines the gateway opens to receive local origin IOUs.
Sequence number
A per account counter that increments with every transaction the account sends. Comparable to an EVM nonce. Native XRPL transactions must use strictly increasing sequence numbers, which serializes activity for a given account. Tickets exist to relax this for parallel work.
SHA 512 half
The XRPL hashing primitive: compute SHA 512 over the input and take the first 256 bits. Used for all transaction and signing hashes. Multi signing prepends the 4 byte SMT\0 prefix and appends the signer's account id before hashing.
SignerList / SignerListSet
The XRPL transaction type that configures or updates an account's multi signing rules. The transaction lists signers (by public key or account id) with weights, and sets a quorum threshold. The bridge uses SignerListSet to rotate the gateway's signer list to the next Axelar verifier set when the set changes.
Ticket
An XRPL primitive that reserves a sequence number for later use. The multisig account periodically issues TicketCreate transactions to pre allocate a batch of up to 250 tickets. Each subsequent outbound payment consumes one ticket from the pool, which allows multiple in flight bridge transactions to be confirmed out of order without sequence collisions. Tickets are owned objects and count against the account reserve while held.
tfPartialPayment
A Payment transaction flag that, when set, allows the delivered amount to be less than the Amount field (XRPL uses this for path finding through illiquid trust lines). The bridge defends against it: the off chain verifiers reject any inbound payment whose delivered_amount is not equal to Amount, and they also reject payments with any flag set. Without this defense an attacker could spend a tiny amount on XRPL and have a much larger transfer credited on the destination chain.
Trust line
The on chain relationship between two XRPL accounts allowing one (the holder) to hold an IOU issued by the other (the issuer). Trust lines are XRPL's mechanism for opt-in token holding: an account never automatically receives an IOU unless it has previously opened a trust line to the issuer with a non-zero limit. The trust line records the holder, the issuer, the currency code, and the maximum amount the holder is willing to hold. It is created and updated by the TrustSet transaction.
Two trust line scenarios apply to the bridge:
- For IOUs that originate on XRPL: the multisig account needs a trust line against the issuer so it can hold and later send the IOU back out on outbound transfers.
- For IOUs that represent assets from another chain: the recipient on XRPL needs a trust line against the multisig account (which is the issuer of these remote-origin IOUs) before they can receive the IOU. Without it the XRPL ledger rejects the inbound
Payment.
TrustSet
The XRPL transaction type that creates or modifies a trust line. Submitted by the holder side of the relationship, citing the issuer account, the currency code, and a numeric limit. Unique to XRPL: it does not exist in EVM, Solana, or other chain models the bridge connects to. Each open trust line is an "owned object" on the holder's account and consumes an owner reserve in XRP, which is why opening many local-IOU trust lines on the multisig account drives the dominant variable cost of running the bridge.
The bridge issues TrustSet transactions in one specific case: when an operator calls TrustSet { token_id } on the XRPL Multisig Prover to allow the multisig account to hold a newly-registered XRPL-native IOU. End-user recipients open their own TrustSet transactions independently of the bridge (using a normal XRPL wallet) before they can receive remote-origin tokens.
XRP
The native currency of the XRPL ledger. Denominated in drops at the protocol level. Used by the bridge both as a transferable asset and as the fee/reserve currency for the multisig account itself.
Axelar Amplifier protocol terms
ampd
The Amplifier off chain daemon that each verifier runs. It maintains the Tendermint RPC connection to the Axelar chain, subscribes to chain events, broadcasts Cosmos transactions, and exposes a local gRPC server for chain specific handler binaries to plug into. Documentation lives upstream.
Amplifier
Axelar's modular cross chain protocol. Anyone can permissionlessly connect a new chain by deploying a small set of CosmWasm contracts on Axelar and integrating with the protocol's verifier set. The XRPL integration is one such connection.
Axelar GMP API
A hosted service that sits in front of the Axelar chain and exposes a single HTTP interface for external chain relayers. It abstracts away the internals of Axelar routing: relayers do not need to talk directly to the CosmWasm contracts, subscribe to Tendermint events, or build Cosmos transactions themselves. Instead they post broadcast requests to the API (targeting a contract address) and pull tasks from it for events emitted by the Axelar chain.
Axelarnet Gateway
The CosmWasm contract on Axelar that lets Axelar resident contracts (such as the ITS Hub) send and receive cross chain messages. Documentation lives upstream.
Coordinator
The CosmWasm contract that owns deployment metadata for each connected chain's contract set and acts as the unbonding gate when a verifier wants to withdraw their stake. See coordinator.
Cross chain id (cc_id)
The identifier shared across chains for a single message. Consists of (chain, id), where chain is the source chain name and id is the chain native message identifier (for XRPL, the transaction hash). The router and gateways use cc_id to track a message end to end.
GMP (general message passing)
The capability for one chain to call a contract on another chain, optionally with attached calldata. Token transfers are a special case of GMP; pure GMP carries only calldata. The bridge supports pure GMP from XRPL (a call_contract payment) but not into XRPL (because XRPL has no executable destination).
ITS (Interchain Token Service)
The Axelar protocol layer on top of GMP that handles cross chain tokens: deployment, linking, and transfers. Documentation lives upstream alongside the ITS Hub and the Solidity ITS edge implementation.
ITS edge
The per chain ITS contract that talks to the ITS Hub. On smart contract chains this is a dedicated contract (Solidity, Move, and so on). On XRPL the role is absorbed by xrpl-gateway.
ITS Hub
The CosmWasm contract on Axelar that routes ITS messages between edges. It validates the source, tracks per chain supply for every token id, rescales amounts for decimal differences between chains, and re wraps messages between the SendToHub and ReceiveFromHub envelopes. Documentation lives upstream.
Multisig contract
The generic CosmWasm contract on Axelar that coordinates signing sessions. The XRPL prover opens sessions on this contract; verifiers submit signature shares; once quorum is met, the contract exposes the assembled signatures for the relayer to fetch. See multisig.
Poll
A verification session opened by a voting verifier contract. A weighted snapshot of the active verifier set is taken from the service registry at the moment the poll is opened, and verifiers vote on the candidate messages. A poll ends when its block expiry is reached; results are stored on chain.
Quorum
The weighted threshold required for a poll or signing session to reach consensus. Configured at the voting verifier level (for polls) and at the prover level (for signing sessions). For XRPL the prover's signing_threshold is mirrored into the XRPL SignerListSet, so that the on ledger multisig rules match the on chain Axelar rules for outbound transactions.
Relayer
An off chain process that watches one chain for events and submits the corresponding messages to the next chain in the GMP path. Relayers are permissionless: anyone can run the same code. For XRPL, a dedicated Rust relayer handles XRPL specific quirks (ticket creation, fee reserves, etc.); its source is at axelarnetwork/axelar-relayer-xrpl.
Router
The CosmWasm contract on Axelar that maintains the registry of connected chains and their gateways and routes verified messages from a source chain's gateway to a destination chain's gateway. See router.
Service Registry
The CosmWasm contract that tracks verifier bonding, authorization, and per chain support. The voting verifier and the prover both query it for the active verifier set when opening polls or signing sessions. See service-registry.
Token id
A 32 byte identifier shared across all chains for a given linked token set. Token ids are derived deterministically from a deployer plus salt or canonical bytes, so the same id can be computed on any chain that knows the inputs. The ITS Hub indexes per chain token state by token id.
Token manager
A per token, per chain construct that defines how tokens move on that chain. Common types are MINT_BURN, MINT_BURN_FROM, LOCK_UNLOCK, LOCK_UNLOCK_FEE, and NATIVE_INTERCHAIN_TOKEN. The XRPL side does not have explicit token manager contracts; the multisig account fills the role implicitly by issuing or burning IOUs and by holding or releasing native XRP.
tofnd
The Axelar key management daemon that ampd talks to over gRPC. It holds verifier signing keys and produces signatures on request, supporting both ECDSA (secp256k1) and Ed25519 (the algorithm is selected per request via the Algorithm field). The Cosmos transactions that ampd broadcasts to the Axelar chain are ECDSA-signed; the XRPL multi-sign shares are also ECDSA, but other chains that the verifier supports may use Ed25519 (for example, Solana). Documentation lives upstream.
Verifier
An off chain operator that participates in the Amplifier protocol by bonding stake in the service registry, voting in polls, and signing in multisig sessions. Verifiers are not chain specific; the same verifier can support many chains by registering chain support for each.
Voting Verifier
The CosmWasm contract that runs polls over messages reported from an external chain. There is one voting verifier per connected chain. For XRPL it is xrpl-voting-verifier, which has an XRPL specific poll variant.
Bridge specific terms
AddGas / AddGasMessage
An XRPL message variant used to top up the gas allocation of an in flight cross chain message that the user underfunded. The user sends an XRPL payment to the multisig account with memo type=add_gas and msg_id=<original message id>. After voting verifier confirmation the additional amount is credited to the gateway's accrued gas tally.
AddReserves / AddReservesMessage
An XRPL message variant used by operators to top up the multisig account's XRP balance so it can keep paying transaction fees and meet its reserve requirements. The operator sends XRP to the multisig account with memo type=add_reserves. After confirmation the prover's tracked fee reserve increases.
CallContractMessage
An XRPL message variant used for pure general message passing from XRPL. The XRPL Payment carries no transferable value beyond the gas fee, but it carries a payload memo whose contents are delivered to the destination contract. There is no inbound counterpart; XRPL cannot host an executable destination.
InterchainTransferMessage
An XRPL message variant used for cross chain token transfers originating on XRPL. The Payment carries the transfer amount plus the gas fee, and optional payload.
ProverMessage
An XRPL specific XRPLMessage (the enum defined in upstream packages/xrpl-types) variant. After the multisig account submits a prover-built XRPL transaction (a Payment, SignerListSet, TicketCreate, or TrustSet) to the ledger, the relayer reports the inclusion (or failure) of that transaction back to Axelar as a ProverMessage. The xrpl-voting-verifier opens a poll on it, verifiers look up the XRPL transaction over JSON RPC and vote, and once quorum is reached the relayer calls ConfirmProverMessage on the xrpl-multisig-prover. That call releases the ticket, clears the stored payload, and (for SignerListSet) promotes the new verifier set. This is the only mechanism by which the Axelar side learns that an outbound XRPL transaction landed, because XRPL has no contract that could emit an event for it.
Wrapped XRP (wXRP)
The on chain representation of native XRP on chains other than XRPL. On XRPL EVM in particular, wXRP is an ERC-20 contract minted by the local ITS edge when XRP arrives over the bridge and burned when XRP leaves to be redeemed for native XRP on XRPL. The bridge treats wXRP and native XRP as two representations of the same token id, with the ITS Hub tracking per chain supply.
xrpl-gateway
The CosmWasm contract on Axelar that acts as the gateway and ITS edge for XRPL traffic. See xrpl-gateway.
xrpl-multisig-prover
The CosmWasm contract on Axelar that builds and tracks XRPL transactions submitted by the multisig account. See xrpl-multisig-prover.
xrpl-voting-verifier
The CosmWasm contract on Axelar that runs polls over XRPL transactions, including outbound prover transactions reported back as ProverMessage. See xrpl-voting-verifier.