PYUSDX

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.

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 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.

Frozen on PYUSDXFrozen on ExtensionDestination TokenBehavior
NoNoPYUSDXMint PYUSDX to recipient
NoNoExtensionMint PYUSDX to Portal, wrap to Extension for recipient
Yes(either)PYUSDXRedirect: mint PYUSDX to fallbackRecipient
Yes(either)ExtensionRedirect: mint PYUSDX to Portal, wrap Extension to fallbackRecipient
NoYesExtensionWrap fails → WrapFailed → mint PYUSDX directly to recipient
Yes(either)Extension (wrap fails)Redirect + WrapFailed: mint PYUSDX directly to fallbackRecipient
Yes (both recipient & fallback)AnyTransaction reverts. Message not marked processed → retryable after admin rotates fallbackRecipient

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.

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.

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 page.

Copyright © M0 Foundation 2026