For the complete documentation index, see llms.txt. This page is also available as Markdown.

34. Transfer between two satellites

A transfer between two satellites moves tokens from a wallet on one satellite chain to a recipient on another. Two chains each do half of the movement and cannot act atomically: the sending chain burns and sends a proof of the burn to the receiving chain, which mints, and each reports its own half. The reference chain authorizes the transfer once and records the change of ownership when both reports have arrived and agree.

The request is the same as for a transfer on one satellite. This guide covers what differs. Read 31 first.

At a glance

Triggered by

Investor (sender), through its ONCHAINID. Or a Token Agent on its behalf

Actors

Investor (sender), Identity Owner, Investor (recipient), Venue or application (as spender), Platform

Contracts

ModularCompliance, compliance modules, Token, TREXRegistry, the ERC-7786 gateways of the two routes, T-REX Lite on both satellites

Preconditions

The asset is open on both satellites, each with its reconciliation window (see 33); issuance is paused for neither chain; the sending wallet holds a bridged position (see 30); the recipient's wallet on the other satellite is linked to an identity that is eligible for the asset

Result

The executed amount has moved from the sender's bridged position on one satellite to the recipient's bridged position on the other, and from the position of the sender's identity to the position of the recipient's identity

Actors and roles

Actor
Role in this flow
What they hold

Investor (sender)

Holds the position on the sending satellite and executes the transfer there, unless a spender does.

A balance on T-REX Lite on the sending satellite

Identity Owner

The sender in its capacity as controller of its ONCHAINID, which requests the validation.

An ACTION key of the identity the sending wallet is linked to

Investor (recipient)

Receives the tokens on the other satellite. Does not transact.

A wallet there, linked to an eligible identity

Venue or application

Executes for the sender, when the validation names it as spender.

Acceptance by the asset's spender policy

Platform

Relays the request and follows both reports to the settlement.

Whatever keys the sender has authorized

Sequence

The burn always executes first. The two reports travel separately and can be delivered to the reference chain in either order.

Step by step

Step 1. Request the validation

Who: Identity Owner (ACTION key), through the ONCHAINID the sending wallet is linked to. Or a Token Agent (AGENT)

Call: ModularCompliance.requestTransferValidation(from, to, requestedMin, requestedMax, spender), with from on one satellite and to on another

Before the call: The pre-checks of 31, step 1, passed, for both chains: Token.isChainOpen is true and ModularCompliance.isIssuancePaused is false for the sender's chain and for the recipient's chain.

What the contract checks: The checks of 31, step 2, with these differences.

  1. Both chains must be open for issuance: neither is paused, and each has a reconciliation window. Otherwise ValidationIssuancePaused or ReconciliationWindowNotSet, naming the chain.

  2. Both chains must be open on the token. Otherwise ChainNotOpen, naming the chain.

What changes: As in 31, with three differences. The validation is recorded as expecting two reports. Its reconciliation window is the larger of the two chains' windows. The token sends the same validation, under the same validationId, to its peer on each chain, and records the gateway used toward each: it emits ValidationRoutePinned and ProtocolMessageSent twice.

Step 2. Execute on the two satellites

Who: Investor (sender), or the spender named in the validation, on the sending satellite

Call: The sender's transfer on T-REX Lite of the sending satellite [[TBC: how a transfer on T-REX Lite names a recipient that is on another satellite]]

Before the call: The validation has been delivered to T-REX Lite on the sending satellite and is stored in its compliance contract.

What the contract checks: On the sending satellite, T-REX Lite executes only if a stored validation matches: the parties match, the amount is inside [amountMin, amountMax], and the execution is strictly before expiry. The validation is consumed there, once. The receiving satellite does not check the validation again. It mints only against a proof of burn that comes, through the trusted gateway, from the asset's T-REX Lite on the sending satellite: without a validation, that burn could not have happened.

What changes: The burn comes first. On the sending satellite, the sender's balance is burned, and T-REX Lite sends a proof of the burn, with the exact amount, to T-REX Lite on the receiving satellite. The receiving satellite mints that amount to the recipient. Each T-REX Lite sends its own settlement notification to the reference chain: the sending chain reports the burn, with from and an empty to. The receiving chain reports the mint, with to and an empty from. Both carry the validationId and the same amount.

Step 3. Record the first report

Who: An ERC-7786 gateway delivers the notification to the Token. No actor transacts

Call: Token.receiveMessage(receiveId, sender, payload), called by the gateway

Before the call: One of the two satellites has executed its half.

What the contract checks: The checks of 31, step 4, with one difference in how the report is matched.

  1. The report is the burn: it carries from, no to, and comes from the sender's chain through the gateway pinned for that chain. Or it is the mint: it carries to, no from, and comes from the recipient's chain through the gateway pinned for that chain. Anything else reverts with SettlementLegMismatch.

What changes: The compliance records the amount and the wallet this report carries, for the other report to match, and emits ValidationLegConfirmed(validationId, chainKey, amount). Ownership does not move yet.

The first report is
Status
On the token

The burn

AwaitingMint

The amount leaves the bridged position of from and is held in transit under the validationId. The reservation on the wallet is released. Events ReleasedFromValidation and HeldInTransit(fromKey, validationId, fromWallet, amount).

The mint

AwaitingBurn

Nothing moves. The reservation on the wallet stays.

In both cases the pending amounts of the two identities stay reserved. From this point the validation can no longer be discarded: one satellite has executed its half, irreversibly.

Step 4. Settle on the second report

Who: An ERC-7786 gateway delivers the notification to the Token. No actor transacts

Call: Token.receiveMessage(receiveId, sender, payload), called by the gateway

Before the call: The other satellite has executed its half.

What the contract checks: The checks of step 3, and:

  1. This half was not already reported. Otherwise nothing is applied, the compliance emits ReplayedSettlement and the token pauses itself.

  2. The amount equals the amount of the first report. Otherwise SettlementAmountMismatch.

What changes: The compliance releases the pending amounts of the two identities and, if the burn is the second report, the reservation on the sending wallet. It moves the executed amount from the position of the sender's identity to the position of the recipient's identity, unless they are the same. On the token, the amount held in transit, or the amount still on the sender's wallet, is credited to the bridged position of to, and the token emits BridgedTransfer(fromKey, toKey, validationId, from, to, amount). The validation becomes Settled and the compliance emits ValidationSettled(validationId, chainKey, amount), with the chain of the report that completed it. The TRACKER modules are told.

State after the flow

  • ModularCompliance.statusOf(validationId) is Settled.

  • Token.bridgedBalanceOf(from) has decreased and Token.bridgedBalanceOf(to) has increased by the executed amount.

  • Token.inTransitOf(validationId) is zero, and Token.totalInTransit() no longer counts it.

  • Token.totalBridged() and Token.totalSupply() are unchanged throughout.

  • ModularCompliance.positionOf has moved by the executed amount between the two identities, unless they are the same.

  • Nothing is reserved any longer for this validation.

Failure modes

Symptom
Cause
Fix

Any revert of the request

Same causes as in 31. The window, pause and route checks apply to both chains (step 1)

See 31. The error names the chain

The delivery of a report reverts with SettlementLegMismatch

The report carries both wallets, the wrong wallet, or comes from the wrong chain (step 3, check 1)

An anomaly of the satellite. The Issuer investigates: 36

The delivery of the second report reverts with SettlementAmountMismatch

The two chains report different amounts under one validation (step 4, check 2)

An anomaly. The validation stays awaiting its second report until it is resolved: 35

The delivery of a report reverts with GatewayNotPinned

The report came through another gateway than the one the validation went out by toward that chain

The report is delivered through the pinned gateway

The status stays AwaitingMint

The receiving chain has not reported. The sender's tokens are burned on the satellite and held in transit on the register, still owned by the sender's identity

Wait. If the report never arrives, the Validation Keeper resolves the validation, and the amount returns to the sender's wallet on the register: 35

The status stays AwaitingBurn

The report of the sending chain has not arrived. The burn itself happened, since the receiving chain minted against its proof

Wait. If the report never arrives, the keeper resolves the validation and issuance is paused for the sending chain

The Validation Keeper cannot discard the validation

One report has arrived. A validation with one half executed is never discarded

Intended. It is settled by the second report, or resolved

The token paused itself

A half that was already reported was delivered again, or a report arrived for a resolved validation (step 4, check 1)

The recipient sees a balance on its satellite and cannot move it

The register has not settled: one report is missing

Wait for the settlement. An onward transfer needs a validation, capped at the position the register records

Common misunderstandings

Wrong: "The tokens cross from one satellite to the other."

Right: No token travels. The sending chain destroys its side and sends a proof of the burn. The receiving chain creates its side against that proof. Both act under one validation issued by the reference chain, and both report to it. The register is where the transfer is recorded.

Wrong: "The transfer is done when the burn is reported."

Right: The burn is half of it. The register takes the amount out of the sender's wallet and holds it in transit: no wallet holds it, and it still belongs to the sender's identity. Ownership moves when the mint is reported too, for the same amount.

Wrong: "The two reports must arrive in order."

Right: The burn executes before the mint, but the two reports travel separately and are accepted in either order. If the mint is reported first, nothing moves on the register until the burn is reported, and the whole movement is then applied at once.

Wrong: "If the second report never arrives, the tokens are lost."

Right: After a second reconciliation window past the release time, the Validation Keeper resolves the validation. When the burn was reported, the amount held in transit returns to the wallet it left on the register. When only the mint was reported, issuance is paused for the sending chain and the Issuer investigates.

Integration notes

  • Follow the transfer with ModularCompliance.statusOf(validationId): Pending, then AwaitingMint or AwaitingBurn, then Settled.

  • Index ValidationLegConfirmed for the first report and ValidationSettled for the second. On the token, index HeldInTransit and BridgedTransfer.

  • Show an amount in transit as still owned by the sender and on its way. Token.inTransitOf(validationId) gives it.

  • The release time of the validation uses the larger of the two reconciliation windows. Read it in validationOf(validationId).releaseAt.

  • Read Token.pinnedRouteFor(validationId, chainKey) for each chain to know which gateway each report must come through.

  • The caller of a transaction pays for the message it sends, directly or through a paymaster: the identity for the validation, on the reference chain, and the caller of the transfer for the burn, its report and the proof of burn, on the sending satellite. The mint on the receiving satellite and its report happen inside the delivery of the proof of burn. [[TBC: who pays for a message that T-REX Lite sends while a delivery is executed]]

  • Between two wallets of one identity on two satellites, the path is the same. The rules about distribution are not asked, and the position of the identity does not change.

Last updated