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.
- Portal (NTT) Repository: m-portal on GitHub
- Portal Lite (Hyperlane) Repository: m-portal-lite on GitHub
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:
Ethereum
↔Arbitrum One
,Ethereum
↔Optimism
- 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
Contract | Network(s) | Address |
---|---|---|
Hub Portal | Ethereum | 0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd |
Spoke Portal | Arbitrum, Optimism | 0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd |
Wormhole Transceiver | All | 0x0763196A091575adF99e2306E5e90E0Be5154841 |
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).
- Approve: Call
approve
on the token contract on the spoke chain, granting an allowance to the Spoke Portal (0xD92...28fd
). - Quote Fee: Obtain the delivery fee for sending a message from the spoke chain to Ethereum.
- Bridge: Call
transferMLikeToken
on the Spoke Portal.destinationChainId
:2
(Wormhole ID for Ethereum).destinationToken
: Thebytes32
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:
Ethereum
↔Plume
,Ethereum
↔HyperEVM
- Chain IDs: Uses standard EVM Chain IDs (
uint256
), e.g., Ethereum =1
, Plume =98866
. - Addresses: All addresses are standard
address
types.
Contract Addresses
Contract | Network(s) | Address |
---|---|---|
Portal Lite | Ethereum, Plume, HyperEVM | 0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE |
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.
- Quote Fee: Call
quoteTransfer
on the Portal Lite contract on Plume to get the fee for delivering a message to Ethereum (destinationChainId = 1
). - Approve: Call
approve
on the token contract on Plume, granting an allowance to the Portal Lite (0x36f...2ecE
). - Bridge: Call
transferMLikeToken
on the Portal Lite on Plume, withdestinationChainId
set to1
.