# PYUSDx Cross-chain (Portal)

> The Portal bridges PYUSDx and its extensions across EVM chains using a burn-and-mint mechanism and pluggable bridge adapters.

## Purpose

The `Portal` contract is the entry point for cross-chain transfers of PYUSDx and its extensions. It uses a **burn-and-mint** mechanism for bridging.

To reduce reliance on the implementations and standards of individual bridging providers, the `Portal` is designed with pluggable bridge adapters.

## Supported chains

The `Portal` is deployed on every chain below. On mainnet, each chain is wired as a peer of the other two, so transfers are possible in both directions along every route. On testnet, Arbitrum Sepolia and Monad Testnet are each peered with Ethereum Sepolia.

<table>
<thead>
  <tr>
    <th>
      Chain
    </th>
    
    <th>
      EVM chain ID
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Ethereum
    </td>
    
    <td>
      <code>
        1
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Arbitrum
    </td>
    
    <td>
      <code>
        42161
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Monad
    </td>
    
    <td>
      <code>
        143
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Ethereum Sepolia
    </td>
    
    <td>
      <code>
        11155111
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Arbitrum Sepolia
    </td>
    
    <td>
      <code>
        421614
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Monad Testnet
    </td>
    
    <td>
      <code>
        10143
      </code>
    </td>
  </tr>
</tbody>
</table>

## Pluggable bridge adapters

The `Portal` delegates cross-chain communication to bridge adapters. Adapters can be implemented using multiple cross-chain protocols (for example LayerZero, Wormhole, or Hyperlane), giving the system the flexibility to switch between them as needed. The `Portal` has no direct knowledge of the underlying bridging implementations when sending or receiving cross-chain messages; all functionality is abstracted behind the adapter interface. Similarly, each bridge adapter has no awareness of protocol-specific logic; it simply sends and receives byte-encoded messages.

If multiple bridge implementations exist for the same pathway, the admin can configure a default option, or a sender can specify the preferred one when sending a cross-chain message.

`LayerZeroBridgeAdapter` is the first adapter implementation. It translates the Portal's generic `sendMessage` / `receiveMessage` interface into LayerZero V2 `Endpoint` calls. Other adapters (Hyperlane, Wormhole, and so on) can be added without modifying the `Portal`.

## Cross-chain token transfer

A cross-chain transfer proceeds as follows:

1. The sender requests a quote by calling `quote` on the `Portal`.
2. The sender approves the `Portal` to spend the `sourceToken`.
3. The sender calls `sendToken`, passing the fee amount from step 1 as `msg.value`.
4. The `Portal` transfers `sourceToken` from the sender.
5. If `sourceToken` is an extension, the `Portal` unwraps it to PYUSDx via `ISwapFacility.swapOut`.
6. The `Portal` burns the PYUSDx via `IPYUSDX.burn`.
7. The `Portal` encodes the token-transfer message.
8. The `Portal` calls `sendMessage` on the `LayerZeroBridgeAdapter`.
9. The `LayerZeroBridgeAdapter` sends the message by calling `send` on the LayerZero `Endpoint`.
10. The LayerZero `Endpoint` on the destination receives the message.
11. The `Endpoint` calls `lzReceive` on the `LayerZeroBridgeAdapter`.
12. The `LayerZeroBridgeAdapter` calls `receiveMessage` on the `Portal`.
13. The `Portal` decodes the payload. It validates that the decoded `targetChainId` matches `currentChainId()` and the decoded `targetBridgeAdapter` matches `msg.sender`, reverting with `InvalidTargetChain` or `InvalidTargetBridgeAdapter` on mismatch (defense-in-depth, independent of LayerZero's own guarantees).
14. If `destinationToken` is PYUSDx, the `Portal` mints it to the recipient.
15. If `destinationToken` is an extension, the `Portal` wraps PYUSDx to it via `ISwapFacility.swapIn` and transfers it to the recipient.
16. If wrapping fails, the recipient receives PYUSDx instead of the extension.

The cross-chain message is a fixed-size, `abi.encodePacked` token-transfer payload carrying the destination chain ID and peer, a message ID, the amount, the destination token, and the sender and recipient. The exact byte layout is on the [PYUSDx specification](/protocol/pyusdx-spec#cross-chain-message-format) page.

## Frozen recipient handling

PYUSDx and each of its extensions maintain independent freeze lists. To prevent total-supply inconsistencies (where tokens are burned on the source chain but cannot be minted on the destination because the recipient becomes frozen while a transfer is in flight), the `Portal` uses a `fallbackRecipient` address. This address is admin-controlled and is expected to remain unfrozen.

The `Portal` checks the freeze list only on PYUSDx. If the recipient is frozen on PYUSDx, the tokens are redirected to the `fallbackRecipient` to prevent a revert during minting. If the recipient is frozen on a PYUSDx extension but not on PYUSDx, the wrapping step via `SwapFacility` fails and the recipient receives PYUSDx directly instead. In practice, any address subject to regulatory restrictions is expected to be frozen consistently across PYUSDx and all of its extensions.

<table>
<thead>
  <tr>
    <th>
      Frozen on PYUSDx
    </th>
    
    <th>
      Frozen on Extension
    </th>
    
    <th>
      Destination Token
    </th>
    
    <th>
      Behavior
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      No
    </td>
    
    <td>
      No
    </td>
    
    <td>
      PYUSDx
    </td>
    
    <td>
      Mint PYUSDx to recipient
    </td>
  </tr>
  
  <tr>
    <td>
      No
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Extension
    </td>
    
    <td>
      Mint PYUSDx to Portal, wrap to Extension for recipient
    </td>
  </tr>
  
  <tr>
    <td>
      Yes
    </td>
    
    <td>
      (either)
    </td>
    
    <td>
      PYUSDx
    </td>
    
    <td>
      <strong>
        Redirect:
      </strong>
      
       mint PYUSDx to <code>
        fallbackRecipient
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Yes
    </td>
    
    <td>
      (either)
    </td>
    
    <td>
      Extension
    </td>
    
    <td>
      <strong>
        Redirect:
      </strong>
      
       mint PYUSDx to Portal, wrap Extension to <code>
        fallbackRecipient
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      No
    </td>
    
    <td>
      Yes
    </td>
    
    <td>
      Extension
    </td>
    
    <td>
      Wrap fails → <code>
        WrapFailed
      </code>
      
       → mint PYUSDx directly to recipient
    </td>
  </tr>
  
  <tr>
    <td>
      Yes
    </td>
    
    <td>
      (either)
    </td>
    
    <td>
      Extension (wrap fails)
    </td>
    
    <td>
      <strong>
        Redirect + WrapFailed:
      </strong>
      
       mint PYUSDx directly to <code>
        fallbackRecipient
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Yes (both recipient & fallback)
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Any
    </td>
    
    <td>
      <strong>
        Transaction reverts.
      </strong>
      
       Message not marked processed → retryable after admin rotates <code>
        fallbackRecipient
      </code>
    </td>
  </tr>
</tbody>
</table>

### Fallback recipient

When the intended recipient of a cross-chain transfer is frozen on PYUSDx, tokens are redirected to the `fallbackRecipient`. This prevents the bridge transaction from reverting and stranding funds. It is set by `DEFAULT_ADMIN_ROLE`.

<note title="Design decision">

`setFallbackRecipient` is gated by `DEFAULT_ADMIN_ROLE` (not `OPERATOR_ROLE`) because the fallback recipient custodies redirected PYUSDx from frozen accounts and must be controlled by the highest-trust role.

</note>

## Pause model

The `Portal` maintains separate send and receive pause states:

- `pauseSend()` / `unpauseSend()`: blocks outbound transfers.
- `pauseReceive()` / `unpauseReceive()`: blocks inbound transfers.
- `pauseAll()` / `unpauseAll()`: both directions.

## Payload gas limit

`setPayloadGasLimit(destinationChainId, gasLimit)` (gated by `OPERATOR_ROLE`) configures the destination-chain execution gas limit per payload type. Passing `gasLimit = 0` **unsets** the limit for that chain; `quote` and `transfer` to that destination then revert until a non-zero limit is configured again.

## Reentrancy protection

The `Portal` uses `ReentrancyLock`, a transient-storage (`TSTORE` / `TLOAD`) guard that stores `msg.sender` as the lock, enabling `msgSender()` to resolve the original caller through the lock.

Deployed contract addresses are listed on the [PYUSDx platform deployments](/resources/addresses/pyusdx-platform) page.
