> For the complete documentation index, see [llms.txt](https://docs.t-rex.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.t-rex.network/build/build-an-erc-7786-gateway-adapter.md).

# 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](/cross-chain-the-distributed-register/messaging-erc-7786-gateways-routes-and-peers.md) and [Messaging providers](/cross-chain-the-distributed-register/messaging-providers.md).

> **Trust statement.** The token acts on the `sender` a 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](/security-and-trust-model/trust-assumptions.md).

## Sending: what the token calls

Every outbound message leaves through one call in `TREXMessaging`, to `sendMessage` of OpenZeppelin's `IERC7786GatewaySource`:

```solidity
function sendMessage(bytes calldata recipient, bytes calldata payload, bytes[] calldata attributes)
    external payable returns (bytes32 sendId);
```

| Element      | What the token passes                                                                                                      |
| ------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `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 `sendMessage` returns, the token takes no further step for that message, whatever the `sendId`.
* **Revert only when the message cannot be carried.** A revert in `sendMessage` reverts 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:

```solidity
function receiveMessage(bytes32 receiveId, bytes calldata sender, bytes calldata payload)
    external payable returns (bytes4);
```

The token checks a delivery in this order.

| #  | Check                                                                                                                                                        | Reverts with                                                                       |
| -- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| 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

| Requirement                                                                                                                             | Reason                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **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](/guides/cross-chain/36.-handle-a-late-reconciliation-or-a-halt.md).

## 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.** `chainType` is `0x0000` and `chainReference` is the chain id in its shortest big-endian form: `0x89` for chain id 137.
* **Chain key.** `MessageTypesLib.chainKey(chainType, chainReference)` is the `keccak256` of that layout with an empty address: `0x00010000018900` is 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 emits `RouteSet(chainKey, gateway)`.
* **Peer.** `Token.setPeer(chainKey, peer)` requires an address with an account part and no trailing bytes (`InvalidPeer`), on the chain of `chainKey` (`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](/cross-chain-the-distributed-register/non-evm-satellites.md).

## The messages an adapter carries

An adapter does not interpret payloads. `MessageTypesLib.Message` has five types.

| Type                      | Direction              | Content                                                                           |
| ------------------------- | ---------------------- | --------------------------------------------------------------------------------- |
| `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](/cross-chain-the-distributed-register/t-rex-lite.md) and [33 Open an asset on a satellite](/guides/cross-chain/33.-open-an-asset-on-a-satellite.md).

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](/cross-chain-the-distributed-register/messaging-providers.md). 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](/guides/identity/07.-link-a-wallet-to-an-onchainid.md).

|                       | Token messaging    | Wallet linking                                                                                       |
| --------------------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
| 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

| Step                                               | Who                                    | Call                                                                                                                                                 |
| -------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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](/guides/cross-chain/33.-open-an-asset-on-a-satellite.md) |

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 `sendMessage` from the token and calls `receiveMessage` on it.
* `sendMessage` works with empty attributes and no value, and attributes the message to `msg.sender`.
* The reported `sender` is 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.t-rex.network/build/build-an-erc-7786-gateway-adapter.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
