Build an ERC-7786 gateway adapter
An adapter is a contract that exposes the ERC-7786 gateway interface on top of a messaging system. Once the network trusts it, an Issuer can route an asset's cross-chain messages through it. This page is for a messaging or interoperability provider, or a team integrating one. It gives the calls the token makes and expects on the reference chain, the checks it applies to every delivery, and what an adapter must guarantee.
Where a gateway sits
A token on the reference chain and its peer on a satellite, T-REX Lite, exchange messages only through ERC-7786 gateways. The protocol calls the interface and nothing below it. Chainlink (CCIP, CRE) is the default provider, connected through an adapter like any other, and an Issuer can route a chain through any other gateway the network trusts. See Messaging: ERC-7786 gateways, routes and peers and Messaging providers.
Trust statement. The token acts on the
sendera trusted gateway reports. A gateway that reports a false sender can impersonate the peer of every asset routed through it. The protocol bounds what such a message can do and does not remove that trust: see Trust assumptions.
Sending: what the token calls
Every outbound message leaves through one call in TREXMessaging, to sendMessage of OpenZeppelin's IERC7786GatewaySource:
function sendMessage(bytes calldata recipient, bytes calldata payload, bytes[] calldata attributes)
external payable returns (bytes32 sendId);msg.sender
The token. It is the only author of protocol messages on the reference chain.
recipient
Token.peerFor(chainKey): the asset's T-REX Lite as an ERC-7930 version 1 address. The destination chain is read from it.
payload
abi.encode(messageType, VERSION, body): the type, a uint8 version equal to 1, and the body as bytes.
attributes
Always an empty array. The token never calls supportsAttribute.
Value
None.
sendId
Emitted in ProtocolMessageSent(messageType, chainKey, sendId). The token does not store it.
Before the call, the token requires a route for the chain, a gateway the registry trusts at that instant, and a known peer. Otherwise it reverts with ChainNotOpen. For a validation it also records the gateway (ValidationRoutePinned) and refuses to send the same validation toward the same chain through another one (ValidationAlreadyRouted).
The call happens inside the transaction that produces the message: ModularCompliance.requestTransferValidation, which sends one message per satellite involved, Token.dispatchMintInstruction or Token.dispatchRecallInstruction.
What to build:
Attribute the message to
msg.sender. The satellite side reports that address, on the reference chain, as the sender.Need no second call from the token. After
sendMessagereturns, the token takes no further step for that message, whatever thesendId.Revert only when the message cannot be carried. A revert in
sendMessagereverts the whole transaction: the validation is not issued.
Receiving: what the adapter calls
The token inherits OpenZeppelin's ERC7786Recipient. The adapter delivers by calling the IERC7786Recipient entry point on the recipient:
The token checks a delivery in this order.
1
The caller is trusted in the TrustedGatewayRegistry at this instant.
ERC7786RecipientUnauthorizedGateway
2
sender parses as an ERC-7930 version 1 address, and names a chain.
InteroperableAddressParsingError, InteroperableAddressEmptyReferenceAndAddress
3
sender is, byte for byte, the asset's peer on the chain it names.
SenderNotPeer
4
The payload decodes to a defined type, with version 1.
No data, or UnsupportedMessageVersion
5
This gateway has not already delivered this receiveId. The pair is then recorded.
MessageAlreadyReceived
6
Settlement: the caller is the gateway the validation went out through toward that chain, or the chain's route if the token never sent that validation there.
GatewayNotPinned, GatewayNotRouted
7
Settlement: the token is not paused.
EnforcedPause
8
Settlement: the compliance matches it against the validation it issued.
SettlementLegMismatch, SettlementOutOfBounds, SettlementAmountMismatch
9
Burn proof: the caller is the chain's route, and the proof names a wallet to credit.
GatewayNotRouted, ZeroAddress
10
Any other type is refused.
MessageTypeNotInbound
A settlement or a burn proof whose body does not decode reverts with no data, after check 5.
On success the token emits ProtocolMessageReceived(messageType, chainKey, receiveId) and returns IERC7786Recipient.receiveMessage.selector. A revert rolls the whole delivery back, the record of check 5 included. Token.messageReceived(gateway, receiveId) tells whether a delivery was accepted.
Checks 6 and 9 compare the caller with the address the token sends through: the contract that delivers to the token and the contract it sends through are one address.
What an adapter must guarantee
Authentic sender. sender is the account that called sendMessage on the source chain.
It is all the token knows about the author. Check 3 and everything after it rest on it.
Canonical sender. One encoding per account: for an EVM account, the output of InteroperableAddress.formatEvmV1(chainId, account).
Check 3 compares bytes. Another encoding of the same account is refused.
Exact recipient and payload, and no fabrication. Only what was sent is delivered, unchanged, to the recipient named.
A validation must reach the asset's T-REX Lite and no other contract. On a settlement for a validation that was never issued, the compliance emits ReplayedSettlement and the token pauses itself.
One receiveId per message, the same on every attempt.
A second delivery under the same id reverts with MessageAlreadyReceived. A settlement delivered again under a new id reaches the compliance: nothing is applied, ReplayedSettlement(validationId, chainKey) is emitted, and the token pauses itself until a Token Agent calls unpause. The Paused event names the adapter. A burn proof delivered again under a new id is accepted again: the record of check 5 is its only guard.
A reverted delivery stays deliverable.
A paused token refuses settlements and expects them after unpause. A settlement also reverts while a TRACKER module of the compliance reverts, and is expected again once that module is removed. A call that reverted is never recorded as delivered.
No ordering, no blocking. A message that reverts does not hold back the others.
The two reports of a transfer between satellites are accepted in either order.
Timely delivery.
A satellite executes a validation strictly before its expiry. A settlement that arrives after its validation was discarded is applied, flagged with LateReconciliation(validationId, chainKey), and pauses issuance for that chain if it breaches a rule. Issuers size the reconciliation window of a chain on the adapter's worst-case latency.
A stable address.
A settlement is accepted only from the address its validation went out through.
The incidents are described in 36 Handle a late reconciliation or a halt.
Chains and addresses
Every chain and account is an ERC-7930 version 1 address: 0x0001, a 2-byte chainType, a 1-byte length, the chainReference, a 1-byte length, the address.
EVM chains.
chainTypeis0x0000andchainReferenceis the chain id in its shortest big-endian form:0x89for chain id 137.Chain key.
MessageTypesLib.chainKey(chainType, chainReference)is thekeccak256of that layout with an empty address:0x00010000018900is hashed for chain id 137. Routes and peers are stored under it.Route.
Token.setRoute(chainType, chainReference, gateway)requires a trusted gateway, or zero to close the chain (GatewayNotTrusted), and a reference that is not empty and, on EVM, has no leading zero byte (InvalidChainReference). It emitsRouteSet(chainKey, gateway).Peer.
Token.setPeer(chainKey, peer)requires an address with an account part and no trailing bytes (InvalidPeer), on the chain ofchainKey(PeerChainMismatch). On an EVM chain with no registered peer, the peer is the token's own address on that chain. On another chain type the chain stays closed until a peer is registered.
One gateway address can be the route of several chains. An adapter that serves a non-EVM chain reports that chain's accounts as canonical ERC-7930 addresses: see Non-EVM satellites.
The messages an adapter carries
An adapter does not interpret payloads. MessageTypesLib.Message has five types.
COMPLIANCE_VALIDATION
To the satellite
The authorization of one movement, which the satellite executes against.
MINT_INSTRUCTION
To the satellite
The instruction to mint a position delegated out. One-way.
RECALL_INSTRUCTION
To the satellite
The instruction to burn a position for a forced recall. Answered by a burn proof.
SETTLEMENT_NOTIFICATION
To the reference chain
The report that a validation, or one leg of it, executed.
BURN_PROOF
To the reference chain
The proof that a position was burned, so that a recall can credit the holder.
Two messages cross gateways without passing through the token. In a transfer between two satellites, the T-REX Lite of the sending chain sends a proof of burn to the T-REX Lite of the receiving chain. When an asset is opened on a satellite, a message reaches the T-REX Lite factory there. An adapter that serves several satellites therefore connects them to each other, and not only to the reference chain. See T-REX Lite and 33 Open an asset on a satellite.
The code sets no maximum payload size. A validation body holds three ERC-7930 addresses of variable length and six fixed fields.
Fees
The caller of the transaction that sends a message pays for it, directly or through a paymaster: see Messaging providers. The token forwards no value and no attribute to sendMessage, and no function on the sending path is payable. An adapter cannot expect msg.value from the token.
[[TBC: how an adapter collects the fee of a message from the caller of the transaction]]
Wallet linking: a second recipient
The IdentityFactory of ONCHAINID is also an ERC7786Recipient. A wallet on another chain proves control of itself by sending a link proposal from that chain, and the adapter delivers it with the same receiveMessage call. See 07 Link a wallet to an ONCHAINID.
Payload
The envelope above
abi.encode(walletEnvelope, identity, expiry)
Required sender
The asset's peer
The wallet itself: sender equals walletEnvelope byte for byte (CrossChainSenderWalletMismatch)
Record of receiveId
Kept by the token
None
What this flow asks of an adapter:
Any account can send. The sender is a holder's wallet, not a contract of the protocol.
No sender on the destination chain. The factory refuses it with
CrossChainLinkForLocalWallet.Deliver once. The factory relies on the gateway for this. A proposal delivered again after the identity declined it is staged again, until its expiry.
The path to being used
The adapter is deployed and reviewed.
The provider, Network governance
It is listed for token messaging.
Network governance (INTEROP_MANAGER)
TrustedGatewayRegistry.setTrustedGateway(gateway, true)
It is listed for wallet linking, per origin chain.
Network governance
IdentityFactory.setTrustedGateway(gateway, chainType, chainReference, true)
It is chosen for an asset and a chain.
Issuer (IDENTITY_MANAGER)
Token.setRoute(chainType, chainReference, gateway): see 33 Open an asset on a satellite
Each list emits its own TrustedGatewaySet event. Tokens read the registry on every send and every delivery: TrustedGatewayRegistry.setTrustedGateway(gateway, false) severs every route through the adapter at once, for every asset, validations in flight included.
Test an adapter
In test/integration/ of the T-REX repository
What it does
mocks/ERC7786GatewayMock.sol
A loopback gateway that never delivers by itself. sendMessage queues. relay(index) delivers an entry, attributed to the account that queued it on the mock's originChainId, under a receiveId that is the same on every relay. drop(index) loses an entry.
helpers/InteropSuiteTest.sol
A deployed suite with the cross-chain settings. _newTrustedGateway(chainId) deploys a mock and lists it. _openEvmChain(token, chainId, gateway) sets the route. _settlement, _burnLeg, _mintLeg and _burnProof build what a T-REX Lite sends, _liteSends and _liteSettles queue it, and _queue queues a message from any author.
interop/InboundMessaging.t.sol
The refusals of the receiving side, the two kinds of replay, the removal of a gateway.
interop/OutboundMessaging.t.sol, interop/RouteSwitch.t.sol
What is sent, route pinning, a change of route with validations in flight.
_proposeCrossChainLink, in helpers/TREXSuiteTest.sol, shows the delivery the IdentityFactory expects.
To test an adapter, extend InteropSuiteTest, list the adapter with trustedGatewayRegistry.setTrustedGateway(address(adapter), true), route a chain to it with _openEvmChain, and replay the cases of InboundMessaging.t.sol through its own delivery path.
Checklist
One address on the reference chain receives
sendMessagefrom the token and callsreceiveMessageon it.sendMessageworks with empty attributes and no value, and attributes the message tomsg.sender.The reported
senderis authentic, in canonical ERC-7930 form, and never an account of the destination chain.Each message has one
receiveId, identical on every attempt.A delivery that reverted is retried later. One that returned, or reverted with
MessageAlreadyReceived, is closed.Nothing is delivered that was not sent, and one stuck message does not block the others.
The worst-case latency per chain is published.