Skip to content

Developer's Guide: Integrating with the M Portals

This guide provides technical details and step-by-step workflows for developers integrating with the M0 token bridging infrastructure. The system uses two distinct protocols for different chains, so it is crucial to use the correct integration path.

The Portal system is built on the Wormhole Native Token Transfer (NTT) standard. It is used for bridging tokens to and from Arbitrum One and Optimism.

Key Concepts

  • Protocol: Wormhole NTT
  • Chains: EthereumArbitrum One, EthereumOptimism
  • Chain IDs: Uses Wormhole Chain IDs (uint16), e.g., Ethereum = 2, Arbitrum = 23, Optimism = 24.
  • Addresses: Recipient and token addresses are passed as bytes32.

Contract Addresses

ContractNetwork(s)Address
Hub PortalEthereum0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd
Spoke PortalArbitrum, Optimism0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd
Wormhole TransceiverAll0x0763196A091575adF99e2306E5e90E0Be5154841

M Portal - Hub to Spoke (e.g., Ethereum → Arbitrum)

This workflow details how to lock a token ($M or w$M) on Ethereum and mint its equivalent on a spoke chain like Arbitrum.

Step 1: Approve the Portal Contract

Before the Portal can transfer your tokens, you must grant it an allowance.

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
// Addresses for the w$M example
address constant W_M_ETHEREUM = 0x437cc33344a0B27A429f795ff6B469C72698B291;
address constant HUB_PORTAL = 0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd;
uint256 amount = 100 * 1e6; // 100 w$M
 
// Approve the Hub Portal to spend your tokens
IERC20(W_M_ETHEREUM).approve(HUB_PORTAL, amount);

Step 2: Quote the Delivery Fee

The Wormhole NTT protocol requires a fee to pay for cross-chain relaying. To get this fee, call the quoteDeliveryPrice function on the Portal contract. The quoted amount must be sent as msg.value in the transferMLikeToken call to ensure the transaction is delivered.

The function takes two parameters:

  • recipientChain (uint16): The destination Wormhole Chain ID (e.g., 23 for Arbitrum).
  • transceiverInstructions (bytes): This parameter is not used and must be passed as an empty bytes array.
// The second argument (transceiverInstructions) is unused and must be empty.
(, uint256 deliveryFee) = IManagerBase(HUB_PORTAL).quoteDeliveryPrice(
    destinationChainId,
    bytes("") // or new bytes(0)
);

Note: For testing, you can often use a nominal value, but production applications must query the correct fee to ensure transaction delivery.

Step 3: Call transferMLikeToken

This function initiates the bridging process. All addresses must be converted to bytes32.

import { IHubPortal } from "./interfaces/IHubPortal.sol";
import { TypeConverter } from "./libs/TypeConverter.sol"; // Assumed helper
 
// Addresses and IDs for the w$M example
address constant W_M_ETHEREUM = 0x437cc33344a0B27A429f795ff6B469C72698B291;
address constant W_M_ARBITRUM = 0x437cc33344a0B27A429f795ff6B469C72698B291;
address constant HUB_PORTAL = 0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd;
uint16 constant ARBITRUM_WORMHOLE_ID = 6;
 
uint256 amount = 100 * 1e6; // 100 w$M
uint256 deliveryFee = ...; // From Step 2
 
IHubPortal(HUB_PORTAL).transferMLikeToken{value: deliveryFee}(
    amount,
    W_M_ETHEREUM, // sourceToken
    ARBITRUM_WORMHOLE_ID, // destinationChainId
    TypeConverter.toBytes32(W_M_ARBITRUM), // destinationToken
    TypeConverter.toBytes32(msg.sender), // recipient
    TypeConverter.toBytes32(msg.sender) // refundAddress
);

M Portal - Spoke to Hub (e.g., Arbitrum → Ethereum)

This workflow is the mirror image of the first. You will interact with the Spoke Portal contract on the source chain (e.g., Arbitrum).

  1. Approve: Call approve on the token contract on the spoke chain, granting an allowance to the Spoke Portal (0xD92...28fd).
  2. Quote Fee: Obtain the delivery fee for sending a message from the spoke chain to Ethereum.
  3. Bridge: Call transferMLikeToken on the Spoke Portal.
    • destinationChainId: 2 (Wormhole ID for Ethereum).
    • destinationToken: The bytes32 address of the target token on Ethereum.

The Portal Lite system is built on the Hyperlane protocol. It is used for bridging tokens to and from Plume and HyperEVM.

Key Concepts

  • Protocol: Hyperlane
  • Chains: EthereumPlume, EthereumHyperEVM
  • Chain IDs: Uses standard EVM Chain IDs (uint256), e.g., Ethereum = 1, Plume = 98866.
  • Addresses: All addresses are standard address types.

Contract Addresses

ContractNetwork(s)Address
Portal LiteEthereum, Plume, HyperEVM0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE

M Portal Lite - Hub to Spoke (e.g., Ethereum → Plume)

This workflow details locking a token on Ethereum and minting its equivalent on Plume.

Step 1: Quote the Delivery Fee

Hyperlane allows on-chain quoting of the message delivery fee. Call this function first to determine the required msg.value.

import { IPortal } from "./interfaces/IPortal.sol"; // From m-portal-lite
 
address constant PORTAL_LITE = 0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE;
uint256 constant PLUME_CHAIN_ID = 98866;
uint256 amount = 100 * 1e6;
address recipient = msg.sender;
 
uint256 deliveryFee = IPortal(PORTAL_LITE).quoteTransfer(
    amount,
    PLUME_CHAIN_ID,
    recipient
);

Step 2: Approve the Portal Contract

Grant the Portal Lite contract an allowance to spend your tokens.

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
address constant W_M_ETHEREUM = 0x437cc33344a0B27A429f795ff6B469C72698B291;
address constant PORTAL_LITE = 0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE;
uint256 amount = 100 * 1e6;
 
IERC20(W_M_ETHEREUM).approve(PORTAL_LITE, amount);

Step 3: Call transferMLikeToken

This function initiates the bridging process, using the fee quoted in Step 1.

import { IPortal } from "./interfaces/IPortal.sol"; // From m-portal-lite
 
// Addresses, IDs, and amounts from previous steps
address constant W_M_ETHEREUM = 0x437cc33344a0B27A429f795ff6B469C72698B291;
address constant W_M_PLUME = 0x437cc33344a0B27A429f795ff6B469C72698B291;
address constant PORTAL_LITE = 0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE;
uint256 constant PLUME_CHAIN_ID = 98866;
uint256 amount = 100 * 1e6;
uint256 deliveryFee = ...; // From Step 1
 
IPortal(PORTAL_LITE).transferMLikeToken{value: deliveryFee}(
    amount,
    W_M_ETHEREUM,    // sourceToken
    PLUME_CHAIN_ID,  // destinationChainId
    W_M_PLUME,       // destinationToken
    msg.sender,      // recipient
    msg.sender       // refundAddress
);

M Portal Lite - Spoke to Hub (e.g., Plume → Ethereum)

The process is identical to Workflow 1 but performed on the spoke chain's contracts.

  1. Quote Fee: Call quoteTransfer on the Portal Lite contract on Plume to get the fee for delivering a message to Ethereum (destinationChainId = 1).
  2. Approve: Call approve on the token contract on Plume, granting an allowance to the Portal Lite (0x36f...2ecE).
  3. Bridge: Call transferMLikeToken on the Portal Lite on Plume, with destinationChainId set to 1.